upd
This commit is contained in:
1 parent
373c832e71
commit
1b0dfdafbc
48 files changed
+3114
-396
No files matched your search
+43
-13
@@ -7,26 +7,56 @@ load_dotenv(dotenv_path='.env')
|
||||
|
||||
PROJECT_HOST = os.getenv('PROJECT_HOST', 'http://127.0.0.1:8080')
|
||||
SANIC_PORT = int(os.getenv('SANIC_PORT', '8080'))
|
||||
UPLOADS_DIR = os.getenv('UPLOADS_DIR', '/app/data')
|
||||
if not os.path.exists(UPLOADS_DIR):
|
||||
os.makedirs(UPLOADS_DIR)
|
||||
|
||||
TELEGRAM_API_KEY = os.environ.get('TELEGRAM_API_KEY')
|
||||
assert TELEGRAM_API_KEY, "Telegram API_KEY required"
|
||||
CLIENT_TELEGRAM_API_KEY = os.environ.get('CLIENT_TELEGRAM_API_KEY')
|
||||
assert CLIENT_TELEGRAM_API_KEY, "Client Telegram API_KEY required"
|
||||
# Use relative path for local development, absolute for container
|
||||
default_uploads = 'data' if not os.path.exists('/app') else '/app/data'
|
||||
UPLOADS_DIR = os.getenv('UPLOADS_DIR', default_uploads)
|
||||
|
||||
# Safe directory creation
|
||||
def safe_mkdir(path: str) -> bool:
|
||||
"""Safely create directory with error handling"""
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return True
|
||||
except (OSError, PermissionError) as e:
|
||||
print(f"Warning: Could not create directory {path}: {e}")
|
||||
return False
|
||||
|
||||
# Try to create uploads directory
|
||||
safe_mkdir(UPLOADS_DIR)
|
||||
|
||||
TELEGRAM_API_KEY = os.environ.get('TELEGRAM_API_KEY', '1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789')
|
||||
CLIENT_TELEGRAM_API_KEY = os.environ.get('CLIENT_TELEGRAM_API_KEY', '1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789')
|
||||
|
||||
import httpx
|
||||
TELEGRAM_BOT_USERNAME = httpx.get(f"https://api.telegram.org/bot{TELEGRAM_API_KEY}/getMe").json()['result']['username']
|
||||
CLIENT_TELEGRAM_BOT_USERNAME = httpx.get(f"https://api.telegram.org/bot{CLIENT_TELEGRAM_API_KEY}/getMe").json()['result']['username']
|
||||
|
||||
# Безопасное получение username с обработкой ошибок
|
||||
def get_bot_username(api_key: str, fallback: str = "unknown_bot") -> str:
|
||||
try:
|
||||
response = httpx.get(f"https://api.telegram.org/bot{api_key}/getMe", timeout=5.0)
|
||||
data = response.json()
|
||||
if response.status_code == 200 and 'result' in data:
|
||||
return data['result']['username']
|
||||
else:
|
||||
print(f"Warning: Failed to get bot username, using fallback. Status: {response.status_code}")
|
||||
return fallback
|
||||
except Exception as e:
|
||||
print(f"Warning: Exception getting bot username: {e}, using fallback")
|
||||
return fallback
|
||||
|
||||
TELEGRAM_BOT_USERNAME = get_bot_username(TELEGRAM_API_KEY, "my_network_bot")
|
||||
CLIENT_TELEGRAM_BOT_USERNAME = get_bot_username(CLIENT_TELEGRAM_API_KEY, "my_client_bot")
|
||||
|
||||
|
||||
MYSQL_URI = os.environ['MYSQL_URI']
|
||||
MYSQL_DATABASE = os.environ['MYSQL_DATABASE']
|
||||
MYSQL_URI = os.environ.get('MYSQL_URI', 'mysql://user:pass@localhost:3306')
|
||||
MYSQL_DATABASE = os.environ.get('MYSQL_DATABASE', 'my_network')
|
||||
|
||||
LOG_LEVEL = os.getenv('LOG_LEVEL', 'DEBUG')
|
||||
LOG_DIR = os.getenv('LOG_DIR', 'logs')
|
||||
if not os.path.exists(LOG_DIR):
|
||||
os.mkdir(LOG_DIR)
|
||||
|
||||
# Safe log directory creation
|
||||
safe_mkdir(LOG_DIR)
|
||||
|
||||
_now_str = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
LOG_FILEPATH = f"{LOG_DIR}/{_now_str}.log"
|
||||
|
||||
@@ -17,8 +17,8 @@ from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.database import get_async_session
|
||||
from app.core.models.content import Content, FileUpload
|
||||
from app.core.database import db_manager
|
||||
from app.core.models.content_models import Content, FileUpload
|
||||
from app.core.storage import storage_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -130,7 +130,7 @@ class ConvertService:
|
||||
|
||||
async def _process_pending_files(self) -> None:
|
||||
"""Process pending file conversions."""
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
try:
|
||||
# Get pending uploads
|
||||
result = await session.execute(
|
||||
@@ -512,7 +512,7 @@ class ConvertService:
|
||||
|
||||
async def _retry_failed_conversions(self) -> None:
|
||||
"""Retry failed conversions that haven't exceeded max retries."""
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
try:
|
||||
# Get failed uploads that can be retried
|
||||
result = await session.execute(
|
||||
@@ -559,7 +559,7 @@ class ConvertService:
|
||||
async def get_processing_stats(self) -> Dict[str, Any]:
|
||||
"""Get processing statistics."""
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
# Get upload stats by status
|
||||
status_result = await session.execute(
|
||||
select(FileUpload.status, asyncio.func.count())
|
||||
|
||||
@@ -11,7 +11,7 @@ from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.database import get_async_session
|
||||
from app.core.database import db_manager
|
||||
from app.core.models.blockchain import Transaction, Wallet, BlockchainNFT, BlockchainTokenBalance
|
||||
from app.core.background.ton_service import TONService
|
||||
|
||||
@@ -174,7 +174,7 @@ class IndexerService:
|
||||
|
||||
async def _index_pending_transactions(self) -> None:
|
||||
"""Index pending transactions from the database."""
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
try:
|
||||
# Get pending transactions
|
||||
result = await session.execute(
|
||||
@@ -230,7 +230,7 @@ class IndexerService:
|
||||
|
||||
async def _update_transaction_confirmations(self) -> None:
|
||||
"""Update confirmation counts for recent transactions."""
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
try:
|
||||
# Get recent confirmed transactions
|
||||
cutoff_time = datetime.utcnow() - timedelta(hours=24)
|
||||
@@ -267,7 +267,7 @@ class IndexerService:
|
||||
|
||||
async def _update_wallet_balances(self) -> None:
|
||||
"""Update wallet balances from the blockchain."""
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
try:
|
||||
# Get active wallets
|
||||
result = await session.execute(
|
||||
@@ -301,7 +301,7 @@ class IndexerService:
|
||||
|
||||
async def _index_nft_collections(self) -> None:
|
||||
"""Index NFT collections and metadata."""
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
try:
|
||||
# Get wallets to check for NFTs
|
||||
result = await session.execute(
|
||||
@@ -370,7 +370,7 @@ class IndexerService:
|
||||
|
||||
async def _update_token_balances(self) -> None:
|
||||
"""Update token balances for wallets."""
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
try:
|
||||
# Get wallets with token balances to update
|
||||
result = await session.execute(
|
||||
@@ -459,7 +459,7 @@ class IndexerService:
|
||||
async def get_indexing_stats(self) -> Dict[str, Any]:
|
||||
"""Get indexing statistics."""
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
# Get transaction stats
|
||||
tx_result = await session.execute(
|
||||
select(Transaction.status, asyncio.func.count())
|
||||
|
||||
@@ -14,7 +14,7 @@ import httpx
|
||||
from sqlalchemy import select, update, and_
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.database import get_async_session, get_cache_manager
|
||||
from app.core.database import db_manager, get_cache_manager
|
||||
from app.core.logging import get_logger
|
||||
from app.core.security import decrypt_data, encrypt_data
|
||||
|
||||
|
||||
+62
-21
@@ -7,7 +7,8 @@ from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseSettings, validator, Field
|
||||
from pydantic import validator, Field
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic.networks import AnyHttpUrl, PostgresDsn, RedisDsn
|
||||
import structlog
|
||||
|
||||
@@ -36,7 +37,7 @@ class Settings(BaseSettings):
|
||||
RATE_LIMIT_ENABLED: bool = Field(default=True)
|
||||
|
||||
# Database
|
||||
DATABASE_URL: PostgresDsn = Field(
|
||||
DATABASE_URL: str = Field(
|
||||
default="postgresql+asyncpg://user:password@localhost:5432/uploader_bot"
|
||||
)
|
||||
DATABASE_POOL_SIZE: int = Field(default=10, ge=1, le=100)
|
||||
@@ -61,10 +62,10 @@ class Settings(BaseSettings):
|
||||
])
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_API_KEY: str = Field(..., min_length=40)
|
||||
CLIENT_TELEGRAM_API_KEY: str = Field(..., min_length=40)
|
||||
TELEGRAM_API_KEY: str = Field(default="1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789")
|
||||
CLIENT_TELEGRAM_API_KEY: str = Field(default="1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789")
|
||||
TELEGRAM_WEBHOOK_ENABLED: bool = Field(default=False)
|
||||
TELEGRAM_WEBHOOK_URL: Optional[AnyHttpUrl] = None
|
||||
TELEGRAM_WEBHOOK_URL: Optional[str] = None
|
||||
TELEGRAM_WEBHOOK_SECRET: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
|
||||
|
||||
# TON Blockchain
|
||||
@@ -76,7 +77,7 @@ class Settings(BaseSettings):
|
||||
MY_FUND_ADDRESS: str = Field(default="UQDarChHFMOI2On9IdHJNeEKttqepgo0AY4bG1trw8OAAwMY")
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL: str = Field(default="INFO", regex="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$")
|
||||
LOG_LEVEL: str = Field(default="INFO", pattern="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$")
|
||||
LOG_DIR: Path = Field(default=Path("logs"))
|
||||
LOG_FORMAT: str = Field(default="json")
|
||||
LOG_ROTATION: str = Field(default="1 day")
|
||||
@@ -112,28 +113,56 @@ class Settings(BaseSettings):
|
||||
|
||||
@validator('UPLOADS_DIR')
|
||||
def create_uploads_dir(cls, v):
|
||||
"""Create uploads directory if it doesn't exist"""
|
||||
if not v.exists():
|
||||
v.mkdir(parents=True, exist_ok=True)
|
||||
"""Create uploads directory if it doesn't exist and is writable"""
|
||||
try:
|
||||
if not v.exists():
|
||||
v.mkdir(parents=True, exist_ok=True)
|
||||
except (OSError, PermissionError) as e:
|
||||
# Handle read-only filesystem or permission errors
|
||||
logger.warning(f"Cannot create uploads directory {v}: {e}")
|
||||
# Use current directory as fallback
|
||||
fallback = Path("./data")
|
||||
try:
|
||||
fallback.mkdir(parents=True, exist_ok=True)
|
||||
return fallback
|
||||
except Exception:
|
||||
# Last fallback - current directory
|
||||
return Path(".")
|
||||
return v
|
||||
|
||||
@validator('LOG_DIR')
|
||||
def create_log_dir(cls, v):
|
||||
"""Create log directory if it doesn't exist"""
|
||||
if not v.exists():
|
||||
v.mkdir(parents=True, exist_ok=True)
|
||||
"""Create log directory if it doesn't exist and is writable"""
|
||||
try:
|
||||
if not v.exists():
|
||||
v.mkdir(parents=True, exist_ok=True)
|
||||
except (OSError, PermissionError) as e:
|
||||
# Handle read-only filesystem or permission errors
|
||||
logger.warning(f"Cannot create log directory {v}: {e}")
|
||||
# Use current directory as fallback
|
||||
fallback = Path("./logs")
|
||||
try:
|
||||
fallback.mkdir(parents=True, exist_ok=True)
|
||||
return fallback
|
||||
except Exception:
|
||||
# Last fallback - current directory
|
||||
return Path(".")
|
||||
return v
|
||||
|
||||
@validator('DATABASE_URL')
|
||||
def validate_database_url(cls, v):
|
||||
"""Validate database URL format"""
|
||||
if not str(v).startswith('postgresql+asyncpg://'):
|
||||
raise ValueError('Database URL must use asyncpg driver')
|
||||
"""Validate database URL format - allow SQLite for testing"""
|
||||
v_str = str(v)
|
||||
if not (v_str.startswith('postgresql+asyncpg://') or v_str.startswith('sqlite+aiosqlite://')):
|
||||
logger.warning(f"Using non-standard database URL: {v_str}")
|
||||
return v
|
||||
|
||||
@validator('TELEGRAM_API_KEY', 'CLIENT_TELEGRAM_API_KEY')
|
||||
def validate_telegram_keys(cls, v):
|
||||
"""Validate Telegram bot tokens format"""
|
||||
"""Validate Telegram bot tokens format - allow test tokens"""
|
||||
if v.startswith('1234567890:'):
|
||||
# Allow test tokens for development
|
||||
return v
|
||||
parts = v.split(':')
|
||||
if len(parts) != 2 or not parts[0].isdigit() or len(parts[1]) != 35:
|
||||
raise ValueError('Invalid Telegram bot token format')
|
||||
@@ -146,10 +175,12 @@ class Settings(BaseSettings):
|
||||
raise ValueError('Secret keys must be at least 32 characters long')
|
||||
return v
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
validate_assignment = True
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
"case_sensitive": True,
|
||||
"validate_assignment": True,
|
||||
"extra": "allow" # Allow extra fields from environment
|
||||
}
|
||||
|
||||
|
||||
class SecurityConfig:
|
||||
@@ -250,4 +281,14 @@ def log_config():
|
||||
logger.info("Configuration loaded", **safe_config)
|
||||
|
||||
# Initialize logging configuration
|
||||
log_config()
|
||||
log_config()
|
||||
|
||||
# Функция для получения настроек (для совместимости с остальным кодом)
|
||||
def get_settings() -> Settings:
|
||||
"""
|
||||
Получить экземпляр настроек приложения.
|
||||
|
||||
Returns:
|
||||
Settings: Конфигурация приложения
|
||||
"""
|
||||
return settings
|
||||
@@ -3,7 +3,8 @@
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from typing import Optional, Dict, Any
|
||||
from pydantic import BaseSettings, Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field, validator
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -13,12 +14,20 @@ class Settings(BaseSettings):
|
||||
app_name: str = Field(default="My Uploader Bot", env="APP_NAME")
|
||||
debug: bool = Field(default=False, env="DEBUG")
|
||||
environment: str = Field(default="production", env="ENVIRONMENT")
|
||||
node_env: str = Field(default="production", env="NODE_ENV")
|
||||
host: str = Field(default="0.0.0.0", env="HOST")
|
||||
port: int = Field(default=15100, env="PORT")
|
||||
|
||||
# API settings
|
||||
api_host: str = Field(default="0.0.0.0", env="API_HOST")
|
||||
api_port: int = Field(default=15100, env="API_PORT")
|
||||
api_workers: int = Field(default=1, env="API_WORKERS")
|
||||
|
||||
# Security settings
|
||||
secret_key: str = Field(env="SECRET_KEY", default="your-secret-key-change-this")
|
||||
jwt_secret_key: str = Field(env="JWT_SECRET_KEY", default="jwt-secret-change-this")
|
||||
jwt_secret: str = Field(env="JWT_SECRET", default="jwt-secret-change-this")
|
||||
encryption_key: str = Field(env="ENCRYPTION_KEY", default="encryption-key-change-this")
|
||||
jwt_algorithm: str = Field(default="HS256", env="JWT_ALGORITHM")
|
||||
jwt_expire_minutes: int = Field(default=30, env="JWT_EXPIRE_MINUTES")
|
||||
|
||||
@@ -41,6 +50,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# Redis settings (new addition)
|
||||
redis_enabled: bool = Field(default=True, env="REDIS_ENABLED")
|
||||
redis_url: str = Field(default="redis://localhost:6379/0", env="REDIS_URL")
|
||||
redis_host: str = Field(default="redis", env="REDIS_HOST")
|
||||
redis_port: int = Field(default=6379, env="REDIS_PORT")
|
||||
redis_password: Optional[str] = Field(default=None, env="REDIS_PASSWORD")
|
||||
@@ -62,6 +72,8 @@ class Settings(BaseSettings):
|
||||
|
||||
# File upload settings
|
||||
max_file_size: int = Field(default=100 * 1024 * 1024, env="MAX_FILE_SIZE") # 100MB
|
||||
max_upload_size: str = Field(default="100MB", env="MAX_UPLOAD_SIZE")
|
||||
upload_path: str = Field(default="./data/uploads", env="UPLOAD_PATH")
|
||||
allowed_extensions: str = Field(default=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.txt", env="ALLOWED_EXTENSIONS")
|
||||
|
||||
# Rate limiting
|
||||
@@ -74,6 +86,19 @@ class Settings(BaseSettings):
|
||||
ton_api_key: Optional[str] = Field(default=None, env="TON_API_KEY")
|
||||
ton_wallet_address: Optional[str] = Field(default=None, env="TON_WALLET_ADDRESS")
|
||||
|
||||
# Telegram Bot settings
|
||||
telegram_api_key: Optional[str] = Field(default=None, env="TELEGRAM_API_KEY")
|
||||
client_telegram_api_key: Optional[str] = Field(default=None, env="CLIENT_TELEGRAM_API_KEY")
|
||||
telegram_webhook_enabled: bool = Field(default=False, env="TELEGRAM_WEBHOOK_ENABLED")
|
||||
|
||||
# MY Network settings
|
||||
my_network_node_id: str = Field(default="local-node", env="MY_NETWORK_NODE_ID")
|
||||
my_network_port: int = Field(default=15100, env="MY_NETWORK_PORT")
|
||||
my_network_host: str = Field(default="0.0.0.0", env="MY_NETWORK_HOST")
|
||||
my_network_domain: str = Field(default="localhost", env="MY_NETWORK_DOMAIN")
|
||||
my_network_ssl_enabled: bool = Field(default=False, env="MY_NETWORK_SSL_ENABLED")
|
||||
my_network_bootstrap_nodes: str = Field(default="", env="MY_NETWORK_BOOTSTRAP_NODES")
|
||||
|
||||
# License settings
|
||||
license_check_enabled: bool = Field(default=True, env="LICENSE_CHECK_ENABLED")
|
||||
license_server_url: Optional[str] = Field(default=None, env="LICENSE_SERVER_URL")
|
||||
@@ -89,10 +114,14 @@ class Settings(BaseSettings):
|
||||
# Logging settings
|
||||
log_level: str = Field(default="INFO", env="LOG_LEVEL")
|
||||
log_format: str = Field(default="json", env="LOG_FORMAT")
|
||||
log_file: str = Field(default="./logs/app.log", env="LOG_FILE")
|
||||
log_file_enabled: bool = Field(default=True, env="LOG_FILE_ENABLED")
|
||||
log_file_max_size: int = Field(default=10 * 1024 * 1024, env="LOG_FILE_MAX_SIZE") # 10MB
|
||||
log_file_backup_count: int = Field(default=5, env="LOG_FILE_BACKUP_COUNT")
|
||||
|
||||
# Maintenance
|
||||
maintenance_mode: bool = Field(default=False, env="MAINTENANCE_MODE")
|
||||
|
||||
# API settings
|
||||
api_title: str = Field(default="My Uploader Bot API", env="API_TITLE")
|
||||
api_version: str = Field(default="1.0.0", env="API_VERSION")
|
||||
|
||||
+14
-5
@@ -237,10 +237,9 @@ db_manager = DatabaseManager()
|
||||
cache_manager: Optional[CacheManager] = None
|
||||
|
||||
|
||||
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Dependency for getting database session"""
|
||||
async with db_manager.get_session() as session:
|
||||
yield session
|
||||
def get_db_session():
|
||||
"""Dependency for getting database session - returns async context manager"""
|
||||
return db_manager.get_session()
|
||||
|
||||
|
||||
async def get_cache() -> CacheManager:
|
||||
@@ -259,4 +258,14 @@ async def init_database():
|
||||
|
||||
async def close_database():
|
||||
"""Close database connections"""
|
||||
await db_manager.close()
|
||||
await db_manager.close()
|
||||
|
||||
|
||||
# Алиасы для совместимости с существующим кодом
|
||||
# УДАЛЁН: get_async_session() - вызывал ошибки context manager protocol
|
||||
# Все места использования исправлены на db_manager.get_session()
|
||||
|
||||
|
||||
async def get_cache_manager() -> CacheManager:
|
||||
"""Alias for get_cache for compatibility"""
|
||||
return await get_cache()
|
||||
@@ -199,10 +199,9 @@ async def get_database_info() -> dict:
|
||||
|
||||
|
||||
# Database session dependency for dependency injection
|
||||
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Database session dependency for API routes."""
|
||||
async with get_async_session() as session:
|
||||
yield session
|
||||
def get_db_session():
|
||||
"""Database session dependency for API routes - returns async context manager"""
|
||||
return get_async_session()
|
||||
|
||||
|
||||
# Backward compatibility functions
|
||||
|
||||
+2
-2
@@ -168,14 +168,14 @@ class DatabaseLogHandler(logging.Handler):
|
||||
|
||||
async def process_logs(self):
|
||||
"""Process logs from queue and store in database"""
|
||||
from app.core.database import get_db_session
|
||||
from app.core.database import db_manager
|
||||
|
||||
while True:
|
||||
try:
|
||||
log_entry = await self._queue.get()
|
||||
|
||||
# Store in database (implement based on your log model)
|
||||
# async with get_db_session() as session:
|
||||
# async with db_manager.get_session() as session:
|
||||
# log_record = LogRecord(**log_entry)
|
||||
# session.add(log_record)
|
||||
# await session.commit()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from app.core.models.base import AlchemyBase
|
||||
from app.core.models.keys import KnownKey
|
||||
from app.core.models.memory import Memory
|
||||
from app.core.models.node_storage import StoredContent
|
||||
# from app.core.models.node_storage import StoredContent # Disabled to avoid conflicts
|
||||
from app.core.models.transaction import UserBalance, InternalTransaction, StarsInvoice
|
||||
from app.core.models.user import User
|
||||
from app.core.models.user.user import User, UserSession, UserRole, UserStatus, ApiKey
|
||||
from app.core.models.wallet_connection import WalletConnection
|
||||
from app.core.models.messages import KnownTelegramMessage
|
||||
from app.core.models.user_activity import UserActivity
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from app.core.models.content.user_content import UserContent
|
||||
@@ -1,48 +1,43 @@
|
||||
|
||||
from sqlalchemy import Column, BigInteger, Integer, String, ForeignKey, DateTime, JSON, Boolean
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, BigInteger, Integer, String, ForeignKey, JSON, Boolean
|
||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.core.models.base import AlchemyBase
|
||||
from app.core.models.content.indexation_mixins import UserContentIndexationMixin
|
||||
from app.core.models.base import BaseModel
|
||||
|
||||
|
||||
class UserContent(AlchemyBase, UserContentIndexationMixin):
|
||||
class UserContent(BaseModel):
|
||||
__tablename__ = 'users_content'
|
||||
|
||||
id = Column(Integer, autoincrement=True, primary_key=True)
|
||||
type = Column(String(128), nullable=False) # 'license/issuer', 'license/listen', 'nft/unknown'
|
||||
onchain_address = Column(String(1024), nullable=True) # bind by this
|
||||
# Legacy compatibility fields
|
||||
type = Column(String(128), nullable=False, default='license/listen')
|
||||
onchain_address = Column(String(1024), nullable=True)
|
||||
owner_address = Column(String(1024), nullable=True)
|
||||
code_hash = Column(String(128), nullable=True)
|
||||
data_hash = Column(String(128), nullable=True)
|
||||
updated = Column(DateTime, nullable=False, default=0)
|
||||
|
||||
content_id = Column(Integer, ForeignKey('node_storage.id'), nullable=True)
|
||||
created = Column(DateTime, nullable=False, default=0)
|
||||
content_id = Column(String(36), ForeignKey('my_network_content.id'), nullable=True)
|
||||
|
||||
meta = Column(JSON, nullable=False, default={})
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
wallet_connection_id = Column(Integer, ForeignKey('wallet_connections.id'), nullable=True)
|
||||
status = Column(String(64), nullable=False, default='active') # 'transaction_requested'
|
||||
meta = Column(JSON, nullable=False, default=dict)
|
||||
user_id = Column(String(36), ForeignKey('users.id'), nullable=False)
|
||||
wallet_connection_id = Column(String(36), ForeignKey('wallet_connections.id'), nullable=True)
|
||||
|
||||
user = relationship('User', uselist=False, foreign_keys=[user_id])
|
||||
wallet_connection = relationship('WalletConnection', uselist=False, foreign_keys=[wallet_connection_id])
|
||||
content = relationship('StoredContent', uselist=False, foreign_keys=[content_id])
|
||||
|
||||
|
||||
class UserAction(AlchemyBase):
|
||||
class UserAction(BaseModel):
|
||||
__tablename__ = 'users_actions'
|
||||
|
||||
id = Column(Integer, autoincrement=True, primary_key=True)
|
||||
type = Column(String(128), nullable=False) # 'purchase'
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
content_id = Column(Integer, ForeignKey('node_storage.id'), nullable=True)
|
||||
user_id = Column(String(36), ForeignKey('users.id'), nullable=False)
|
||||
content_id = Column(String(36), ForeignKey('my_network_content.id'), nullable=True)
|
||||
telegram_message_id = Column(BigInteger, nullable=True)
|
||||
|
||||
to_address = Column(String(1024), nullable=True)
|
||||
from_address = Column(String(1024), nullable=True)
|
||||
status = Column(String(128), nullable=True)
|
||||
meta = Column(JSON, nullable=False, default={})
|
||||
created = Column(DateTime, nullable=False, default=0)
|
||||
meta = Column(JSON, nullable=False, default=dict)
|
||||
|
||||
user = relationship('User', uselist=False, foreign_keys=[user_id])
|
||||
content = relationship('StoredContent', uselist=False, foreign_keys=[content_id])
|
||||
|
||||
@@ -3,13 +3,13 @@ Content models with async support and enhanced features
|
||||
"""
|
||||
import hashlib
|
||||
import mimetypes
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any, Union
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from sqlalchemy import Column, String, Integer, BigInteger, Boolean, Text, ForeignKey, Index, text
|
||||
from sqlalchemy import Column, String, Integer, BigInteger, Boolean, Text, ForeignKey, Index, text, DateTime
|
||||
from sqlalchemy.dialects.postgresql import JSONB, ARRAY
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
@@ -61,7 +61,7 @@ class LicenseType(str, Enum):
|
||||
class StoredContent(BaseModel):
|
||||
"""Enhanced content storage model"""
|
||||
|
||||
__tablename__ = 'stored_content'
|
||||
__tablename__ = 'my_network_content'
|
||||
|
||||
# Content identification
|
||||
hash = Column(
|
||||
@@ -487,7 +487,7 @@ class UserContent(BaseModel):
|
||||
# Content relationship
|
||||
content_id = Column(
|
||||
String(36), # UUID
|
||||
ForeignKey('stored_content.id'),
|
||||
ForeignKey('my_network_content.id'),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="Reference to stored content"
|
||||
@@ -728,4 +728,174 @@ class EncryptionKey(BaseModel):
|
||||
|
||||
def revoke(self) -> None:
|
||||
"""Revoke the key"""
|
||||
self.revoked_at = datetime.utcnow()
|
||||
self.revoked_at = datetime.utcnow()
|
||||
|
||||
|
||||
# Backward compatibility aliases
|
||||
Content = StoredContent
|
||||
|
||||
|
||||
class ContentChunk(BaseModel):
|
||||
"""Content chunk for large file uploads"""
|
||||
|
||||
__tablename__ = 'content_chunks'
|
||||
|
||||
# Chunk identification
|
||||
content_id = Column(
|
||||
String(36), # UUID
|
||||
ForeignKey('my_network_content.id'),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="Parent content ID"
|
||||
)
|
||||
chunk_index = Column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
comment="Chunk sequence number"
|
||||
)
|
||||
chunk_hash = Column(
|
||||
String(128),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="Hash of this chunk"
|
||||
)
|
||||
|
||||
# Chunk data
|
||||
chunk_size = Column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
comment="Size of this chunk in bytes"
|
||||
)
|
||||
chunk_data = Column(
|
||||
Text,
|
||||
nullable=True,
|
||||
comment="Base64 encoded chunk data (for small chunks)"
|
||||
)
|
||||
file_path = Column(
|
||||
String(1024),
|
||||
nullable=True,
|
||||
comment="Path to chunk file (for large chunks)"
|
||||
)
|
||||
|
||||
# Upload status
|
||||
uploaded = Column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="Whether chunk is uploaded"
|
||||
)
|
||||
|
||||
# Relationships
|
||||
content = relationship('StoredContent', back_populates='chunks')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"ContentChunk({self.id}, content={self.content_id}, index={self.chunk_index})"
|
||||
|
||||
|
||||
class FileUpload(BaseModel):
|
||||
"""File upload session tracking"""
|
||||
|
||||
__tablename__ = 'file_uploads'
|
||||
|
||||
# Upload identification
|
||||
upload_id = Column(
|
||||
String(128),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
index=True,
|
||||
comment="Unique upload session ID"
|
||||
)
|
||||
filename = Column(
|
||||
String(512),
|
||||
nullable=False,
|
||||
comment="Original filename"
|
||||
)
|
||||
|
||||
# Upload metadata
|
||||
total_size = Column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
comment="Total file size in bytes"
|
||||
)
|
||||
uploaded_size = Column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
default=0,
|
||||
comment="Uploaded size in bytes"
|
||||
)
|
||||
chunk_size = Column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=1048576, # 1MB
|
||||
comment="Chunk size in bytes"
|
||||
)
|
||||
total_chunks = Column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
comment="Total number of chunks"
|
||||
)
|
||||
uploaded_chunks = Column(
|
||||
Integer,
|
||||
nullable=False,
|
||||
default=0,
|
||||
comment="Number of uploaded chunks"
|
||||
)
|
||||
|
||||
# Upload status
|
||||
upload_status = Column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
default='pending',
|
||||
comment="Upload status"
|
||||
)
|
||||
|
||||
# User information
|
||||
user_id = Column(
|
||||
String(36), # UUID
|
||||
ForeignKey('users.id'),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="User performing the upload"
|
||||
)
|
||||
|
||||
# Completion
|
||||
content_id = Column(
|
||||
String(36), # UUID
|
||||
ForeignKey('my_network_content.id'),
|
||||
nullable=True,
|
||||
comment="Final content ID after completion"
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user = relationship('User', back_populates='file_uploads')
|
||||
content = relationship('StoredContent', back_populates='file_upload')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"FileUpload({self.id}, upload_id={self.upload_id}, status={self.upload_status})"
|
||||
|
||||
@property
|
||||
def progress_percentage(self) -> float:
|
||||
"""Get upload progress percentage"""
|
||||
if self.total_size == 0:
|
||||
return 0.0
|
||||
return (self.uploaded_size / self.total_size) * 100.0
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
"""Check if upload is complete"""
|
||||
return self.uploaded_size >= self.total_size and self.upload_status == 'completed'
|
||||
|
||||
def update_progress(self, chunk_size: int) -> None:
|
||||
"""Update upload progress"""
|
||||
self.uploaded_size += chunk_size
|
||||
self.uploaded_chunks += 1
|
||||
|
||||
if self.uploaded_size >= self.total_size:
|
||||
self.upload_status = 'completed'
|
||||
elif self.upload_status == 'pending':
|
||||
self.upload_status = 'uploading'
|
||||
|
||||
|
||||
# Update relationships in StoredContent
|
||||
StoredContent.chunks = relationship('ContentChunk', back_populates='content')
|
||||
StoredContent.file_upload = relationship('FileUpload', back_populates='content', uselist=False)
|
||||
@@ -1,35 +1,13 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, BigInteger, DateTime, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
# Import and re-export models from the user.py module inside this directory
|
||||
from .user import User, UserSession, UserRole, UserStatus, ApiKey
|
||||
|
||||
from app.core.auth_v1 import AuthenticationMixin as AuthenticationMixin_V1
|
||||
from app.core.models.user.display_mixin import DisplayMixin
|
||||
from app.core.models.user.wallet_mixin import WalletMixin
|
||||
from app.core.translation import TranslationCore
|
||||
from ..base import AlchemyBase
|
||||
|
||||
|
||||
class User(AlchemyBase, DisplayMixin, TranslationCore, AuthenticationMixin_V1, WalletMixin):
|
||||
LOCALE_DOMAIN = 'sanic_telegram_bot'
|
||||
|
||||
__tablename__ = 'users'
|
||||
id = Column(Integer, autoincrement=True, primary_key=True)
|
||||
telegram_id = Column(BigInteger, nullable=False)
|
||||
|
||||
username = Column(String(512), nullable=True)
|
||||
lang_code = Column(String(8), nullable=False, default="en")
|
||||
meta = Column(JSON, nullable=False, default={})
|
||||
|
||||
last_use = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
updated = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
created = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
balances = relationship('UserBalance', back_populates='user')
|
||||
internal_transactions = relationship('InternalTransaction', back_populates='user')
|
||||
wallet_connections = relationship('WalletConnection', back_populates='user')
|
||||
# stored_content = relationship('StoredContent', back_populates='user')
|
||||
|
||||
def __str__(self):
|
||||
return f"User, {self.id}_{self.telegram_id} | Username: {self.username} " + '\\'
|
||||
# Keep backward compatibility
|
||||
__all__ = [
|
||||
'User',
|
||||
'UserSession',
|
||||
'UserRole',
|
||||
'UserStatus',
|
||||
'ApiKey'
|
||||
]
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import Column, String, BigInteger, Boolean, Integer, Index, text
|
||||
from sqlalchemy import Column, String, BigInteger, Boolean, Integer, Index, text, DateTime
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
@@ -181,7 +181,7 @@ class User(BaseModel):
|
||||
Index('idx_users_telegram_id', 'telegram_id'),
|
||||
Index('idx_users_username', 'username'),
|
||||
Index('idx_users_role_status', 'role', 'status'),
|
||||
Index('idx_users_last_activity', 'last_activity'),
|
||||
Index('idx_users_last_activity', 'last_use'), # Use actual database column name
|
||||
Index('idx_users_created_at', 'created_at'),
|
||||
)
|
||||
|
||||
@@ -417,4 +417,181 @@ class User(BaseModel):
|
||||
'is_verified': self.is_verified,
|
||||
'is_premium': self.is_premium,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserSession(BaseModel):
|
||||
"""User session model for authentication tracking"""
|
||||
|
||||
__tablename__ = 'user_sessions'
|
||||
|
||||
user_id = Column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="Associated user ID"
|
||||
)
|
||||
refresh_token_hash = Column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
comment="Hashed refresh token"
|
||||
)
|
||||
ip_address = Column(
|
||||
String(45),
|
||||
nullable=True,
|
||||
comment="Session IP address"
|
||||
)
|
||||
user_agent = Column(
|
||||
String(512),
|
||||
nullable=True,
|
||||
comment="User agent string"
|
||||
)
|
||||
expires_at = Column(
|
||||
DateTime,
|
||||
nullable=False,
|
||||
comment="Session expiration time"
|
||||
)
|
||||
last_used_at = Column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="Last time session was used"
|
||||
)
|
||||
logged_out_at = Column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="Session logout time"
|
||||
)
|
||||
is_active = Column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=True,
|
||||
comment="Whether session is active"
|
||||
)
|
||||
remember_me = Column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="Whether this is a remember me session"
|
||||
)
|
||||
|
||||
# Indexes for performance
|
||||
__table_args__ = (
|
||||
Index('idx_user_sessions_user_id', 'user_id'),
|
||||
Index('idx_user_sessions_expires_at', 'expires_at'),
|
||||
Index('idx_user_sessions_active', 'is_active'),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"UserSession({self.id}, user_id={self.user_id}, active={self.is_active})"
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if session is expired"""
|
||||
return datetime.utcnow() > self.expires_at
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if session is valid and active"""
|
||||
return self.is_active and not self.is_expired() and not self.logged_out_at
|
||||
|
||||
|
||||
class UserRole(BaseModel):
|
||||
"""User role model for permissions"""
|
||||
|
||||
__tablename__ = 'user_roles'
|
||||
|
||||
name = Column(
|
||||
String(64),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
index=True,
|
||||
comment="Role name"
|
||||
)
|
||||
description = Column(
|
||||
String(255),
|
||||
nullable=True,
|
||||
comment="Role description"
|
||||
)
|
||||
permissions = Column(
|
||||
ARRAY(String),
|
||||
nullable=False,
|
||||
default=list,
|
||||
comment="Role permissions list"
|
||||
)
|
||||
is_system = Column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
comment="Whether this is a system role"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"UserRole({self.name})"
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""Check if role has specific permission"""
|
||||
return permission in (self.permissions or [])
|
||||
|
||||
|
||||
class ApiKey(BaseModel):
|
||||
"""API key model for programmatic access"""
|
||||
|
||||
__tablename__ = 'api_keys'
|
||||
|
||||
user_id = Column(
|
||||
BigInteger,
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="Associated user ID"
|
||||
)
|
||||
name = Column(
|
||||
String(128),
|
||||
nullable=False,
|
||||
comment="API key name"
|
||||
)
|
||||
key_hash = Column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
comment="Hashed API key"
|
||||
)
|
||||
permissions = Column(
|
||||
ARRAY(String),
|
||||
nullable=False,
|
||||
default=list,
|
||||
comment="API key permissions"
|
||||
)
|
||||
expires_at = Column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="API key expiration time"
|
||||
)
|
||||
last_used_at = Column(
|
||||
DateTime,
|
||||
nullable=True,
|
||||
comment="Last time key was used"
|
||||
)
|
||||
is_active = Column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=True,
|
||||
comment="Whether key is active"
|
||||
)
|
||||
|
||||
# Indexes for performance
|
||||
__table_args__ = (
|
||||
Index('idx_api_keys_user_id', 'user_id'),
|
||||
Index('idx_api_keys_hash', 'key_hash'),
|
||||
Index('idx_api_keys_active', 'is_active'),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"ApiKey({self.id}, name={self.name}, user_id={self.user_id})"
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if API key is expired"""
|
||||
if not self.expires_at:
|
||||
return False
|
||||
return datetime.utcnow() > self.expires_at
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Check if API key is valid and active"""
|
||||
return self.is_active and not self.is_expired()
|
||||
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Set, Any
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.database_compatible import get_async_session
|
||||
from app.core.database import db_manager
|
||||
from app.core.models.content_compatible import Content
|
||||
from app.core.cache import cache
|
||||
from .bootstrap_manager import BootstrapManager
|
||||
@@ -297,8 +297,11 @@ class MyNetworkNodeService:
|
||||
logger.info(f"Starting replication of content: {content_hash}")
|
||||
|
||||
# Найти контент в локальной БД
|
||||
async with get_async_session() as session:
|
||||
content = await session.get(Content, {"hash": content_hash})
|
||||
async with db_manager.get_session() as session:
|
||||
from sqlalchemy import select
|
||||
stmt = select(Content).where(Content.hash == content_hash)
|
||||
result = await session.execute(stmt)
|
||||
content = result.scalar_one_or_none()
|
||||
|
||||
if not content:
|
||||
raise ValueError(f"Content not found: {content_hash}")
|
||||
@@ -358,6 +361,54 @@ class MyNetworkNodeService:
|
||||
async def get_content_sync_status(self, content_hash: str) -> Dict[str, Any]:
|
||||
"""Получить статус синхронизации конкретного контента."""
|
||||
return await self.sync_manager.get_content_sync_status(content_hash)
|
||||
|
||||
async def get_node_info(self) -> Dict[str, Any]:
|
||||
"""Получить информацию о текущей ноде."""
|
||||
try:
|
||||
uptime_seconds = self._get_uptime_hours() * 3600 if self.start_time else 0
|
||||
|
||||
return {
|
||||
"node_id": self.node_id,
|
||||
"status": "running" if self.is_running else "stopped",
|
||||
"version": "2.0",
|
||||
"uptime": uptime_seconds,
|
||||
"start_time": self.start_time.isoformat() if self.start_time else None,
|
||||
"metrics": self.node_metrics.copy(),
|
||||
"storage_path": str(self.storage_path),
|
||||
"last_sync": self.last_sync_time.isoformat() if self.last_sync_time else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting node info: {e}")
|
||||
return {
|
||||
"node_id": self.node_id,
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def get_peers_info(self) -> Dict[str, Any]:
|
||||
"""Получить информацию о пирах."""
|
||||
try:
|
||||
connected_peers = self.peer_manager.get_connected_peers()
|
||||
all_peers_info = self.peer_manager.get_all_peers_info()
|
||||
connection_stats = self.peer_manager.get_connection_stats()
|
||||
|
||||
return {
|
||||
"peer_count": len(connected_peers),
|
||||
"connected_peers": list(connected_peers),
|
||||
"peers": list(all_peers_info.values()),
|
||||
"connection_stats": connection_stats,
|
||||
"healthy_connections": connection_stats.get("healthy_connections", 0),
|
||||
"total_connections": connection_stats.get("total_connections", 0),
|
||||
"average_latency_ms": connection_stats.get("average_latency_ms")
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting peers info: {e}")
|
||||
return {
|
||||
"peer_count": 0,
|
||||
"connected_peers": [],
|
||||
"peers": [],
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
# Глобальный экземпляр сервиса ноды
|
||||
|
||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
||||
from typing import Dict, List, Optional, Any, Set
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
from app.core.database_compatible import get_async_session
|
||||
from app.core.database import db_manager
|
||||
from app.core.models.content_compatible import Content, ContentMetadata
|
||||
from app.core.cache import cache
|
||||
|
||||
@@ -328,9 +328,11 @@ class ContentSyncManager:
|
||||
async def _get_local_content_info(self, content_hash: str) -> Optional[Dict[str, Any]]:
|
||||
"""Получить информацию о локальном контенте."""
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
# Найти контент по хешу
|
||||
stmt = select(Content).where(Content.md5_hash == content_hash or Content.sha256_hash == content_hash)
|
||||
stmt = select(Content).where(
|
||||
(Content.md5_hash == content_hash) | (Content.sha256_hash == content_hash)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
content = result.scalar_one_or_none()
|
||||
|
||||
@@ -352,7 +354,7 @@ class ContentSyncManager:
|
||||
"file_type": content.file_type,
|
||||
"mime_type": content.mime_type,
|
||||
"encrypted": content.encrypted if hasattr(content, 'encrypted') else False,
|
||||
"metadata": metadata.to_dict() if metadata else {}
|
||||
"metadata": metadata.to_dict() if metadata and hasattr(metadata, 'to_dict') else {}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -484,7 +486,7 @@ class ContentSyncManager:
|
||||
async def _save_content_to_db(self, content_hash: str, file_path: Path, metadata: Dict[str, Any]) -> None:
|
||||
"""Сохранить информацию о контенте в базу данных."""
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
# Создать запись контента
|
||||
content = Content(
|
||||
filename=metadata.get("filename", file_path.name),
|
||||
@@ -647,16 +649,16 @@ class ContentSyncManager:
|
||||
async def _get_local_content_hashes(self) -> Set[str]:
|
||||
"""Получить множество хешей локального контента."""
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
stmt = select(Content.md5_hash, Content.sha256_hash).where(Content.is_active == True)
|
||||
result = await session.execute(stmt)
|
||||
|
||||
hashes = set()
|
||||
for row in result:
|
||||
if row.md5_hash:
|
||||
hashes.add(row.md5_hash)
|
||||
if row.sha256_hash:
|
||||
hashes.add(row.sha256_hash)
|
||||
if row[0]: # md5_hash
|
||||
hashes.add(row[0])
|
||||
if row[1]: # sha256_hash
|
||||
hashes.add(row[1])
|
||||
|
||||
return hashes
|
||||
|
||||
|
||||
+13
-13
@@ -18,9 +18,9 @@ from sqlalchemy import select, update
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.config import get_settings
|
||||
from app.core.database import get_async_session, get_cache_manager
|
||||
from app.core.database import db_manager, get_cache_manager
|
||||
from app.core.logging import get_logger
|
||||
from app.core.models.content import Content, ContentChunk
|
||||
from app.core.models.content_models import Content, ContentChunk
|
||||
from app.core.security import encrypt_file, decrypt_file, generate_access_token
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -227,7 +227,7 @@ class StorageManager:
|
||||
await self.cache_manager.set(session_key, session_data, ttl=86400) # 24 hours
|
||||
|
||||
# Store in database for persistence
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
upload_session = ContentUploadSession(
|
||||
id=upload_id,
|
||||
content_id=content_id,
|
||||
@@ -296,7 +296,7 @@ class StorageManager:
|
||||
await self.cache_manager.set(session_key, session_data, ttl=86400)
|
||||
|
||||
# Store chunk info in database
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
chunk_record = ContentChunk(
|
||||
upload_id=upload_id,
|
||||
chunk_index=chunk_index,
|
||||
@@ -347,7 +347,7 @@ class StorageManager:
|
||||
raise ValueError(f"Missing chunks: {missing_chunks}")
|
||||
|
||||
# Get chunk IDs in order
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
stmt = (
|
||||
select(ContentChunk)
|
||||
.where(ContentChunk.upload_id == upload_id)
|
||||
@@ -362,7 +362,7 @@ class StorageManager:
|
||||
file_path = await self.backend.assemble_file(upload_id, chunk_ids)
|
||||
|
||||
# Update content record
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
stmt = (
|
||||
update(Content)
|
||||
.where(Content.id == UUID(session_data["content_id"]))
|
||||
@@ -416,7 +416,7 @@ class StorageManager:
|
||||
async def delete_content_files(self, content_id: UUID) -> bool:
|
||||
"""Delete all files associated with content."""
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
# Get content
|
||||
stmt = select(Content).where(Content.id == content_id)
|
||||
result = await session.execute(stmt)
|
||||
@@ -465,7 +465,7 @@ class StorageManager:
|
||||
async def get_storage_stats(self) -> Dict[str, Any]:
|
||||
"""Get storage usage statistics."""
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
# Get total files and size
|
||||
from sqlalchemy import func
|
||||
stmt = select(
|
||||
@@ -517,7 +517,7 @@ class StorageManager:
|
||||
|
||||
# Fallback to database
|
||||
try:
|
||||
async with get_async_session() as session:
|
||||
async with db_manager.get_session() as session:
|
||||
stmt = (
|
||||
select(ContentUploadSession)
|
||||
.where(ContentUploadSession.id == upload_id)
|
||||
@@ -559,14 +559,14 @@ class StorageManager:
|
||||
return None
|
||||
|
||||
# Additional model for upload sessions
|
||||
from app.core.models.base import Base
|
||||
from sqlalchemy import Column, Integer, DateTime
|
||||
from app.core.models.base import BaseModel
|
||||
from sqlalchemy import Column, Integer, DateTime, String
|
||||
|
||||
class ContentUploadSession(Base):
|
||||
class ContentUploadSession(BaseModel):
|
||||
"""Model for tracking upload sessions."""
|
||||
__tablename__ = "content_upload_sessions"
|
||||
|
||||
content_id = Column("content_id", sa.UUID(as_uuid=True), nullable=False)
|
||||
content_id = Column("content_id", String(36), nullable=False)
|
||||
total_size = Column(Integer, nullable=False)
|
||||
chunk_size = Column(Integer, nullable=False, default=1048576) # 1MB
|
||||
total_chunks = Column(Integer, nullable=False)
|
||||
|
||||
+14
-13
@@ -8,7 +8,7 @@ from typing import Dict, List, Optional, Any, Union
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field, validator, root_validator
|
||||
from pydantic import BaseModel, Field, validator, model_validator
|
||||
from pydantic.networks import EmailStr, HttpUrl
|
||||
|
||||
class ContentTypeEnum(str, Enum):
|
||||
@@ -45,14 +45,15 @@ class PermissionEnum(str, Enum):
|
||||
class BaseSchema(BaseModel):
|
||||
"""Base schema with common configuration."""
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
validate_assignment = True
|
||||
allow_population_by_field_name = True
|
||||
json_encoders = {
|
||||
model_config = {
|
||||
"use_enum_values": True,
|
||||
"validate_assignment": True,
|
||||
"populate_by_name": True,
|
||||
"json_encoders": {
|
||||
datetime: lambda v: v.isoformat(),
|
||||
UUID: lambda v: str(v)
|
||||
}
|
||||
}
|
||||
|
||||
class ContentSchema(BaseSchema):
|
||||
"""Schema for content creation."""
|
||||
@@ -133,12 +134,12 @@ class ContentSearchSchema(BaseSchema):
|
||||
visibility: Optional[VisibilityEnum] = None
|
||||
date_from: Optional[datetime] = None
|
||||
date_to: Optional[datetime] = None
|
||||
sort_by: Optional[str] = Field("updated_at", regex="^(created_at|updated_at|title|file_size)$")
|
||||
sort_order: Optional[str] = Field("desc", regex="^(asc|desc)$")
|
||||
sort_by: Optional[str] = Field("updated_at", pattern="^(created_at|updated_at|title|file_size)$")
|
||||
sort_order: Optional[str] = Field("desc", pattern="^(asc|desc)$")
|
||||
page: int = Field(1, ge=1, le=1000)
|
||||
per_page: int = Field(20, ge=1, le=100)
|
||||
|
||||
@root_validator
|
||||
@model_validator(mode='before')
|
||||
def validate_date_range(cls, values):
|
||||
"""Validate date range."""
|
||||
date_from = values.get('date_from')
|
||||
@@ -151,7 +152,7 @@ class ContentSearchSchema(BaseSchema):
|
||||
|
||||
class UserRegistrationSchema(BaseSchema):
|
||||
"""Schema for user registration."""
|
||||
username: str = Field(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9_.-]+$")
|
||||
username: str = Field(..., min_length=3, max_length=50, pattern="^[a-zA-Z0-9_.-]+$")
|
||||
email: EmailStr = Field(..., description="Valid email address")
|
||||
password: str = Field(..., min_length=8, max_length=128, description="Password (min 8 characters)")
|
||||
full_name: Optional[str] = Field(None, max_length=100)
|
||||
@@ -246,7 +247,7 @@ class ChunkUploadSchema(BaseSchema):
|
||||
|
||||
class BlockchainTransactionSchema(BaseSchema):
|
||||
"""Schema for blockchain transactions."""
|
||||
transaction_type: str = Field(..., regex="^(transfer|mint|burn|stake|unstake)$")
|
||||
transaction_type: str = Field(..., pattern="^(transfer|mint|burn|stake|unstake)$")
|
||||
amount: Optional[int] = Field(None, ge=0, description="Amount in nanotons")
|
||||
recipient_address: Optional[str] = Field(None, min_length=48, max_length=48)
|
||||
message: Optional[str] = Field(None, max_length=500)
|
||||
@@ -276,10 +277,10 @@ class LicenseSchema(BaseSchema):
|
||||
class AccessControlSchema(BaseSchema):
|
||||
"""Schema for content access control."""
|
||||
user_id: UUID = Field(..., description="User to grant access to")
|
||||
permission: str = Field(..., regex="^(read|write|delete|admin)$")
|
||||
permission: str = Field(..., pattern="^(read|write|delete|admin)$")
|
||||
expires_at: Optional[datetime] = Field(None, description="Access expiration time")
|
||||
|
||||
@root_validator
|
||||
@model_validator(mode='before')
|
||||
def validate_expiration(cls, values):
|
||||
"""Validate access expiration."""
|
||||
expires_at = values.get('expires_at')
|
||||
|
||||
Reference in new issue
Block a user