""" app.py — Flask API rbxl-importer. Endpoints: POST /import/rbxl/analyze body: multipart, file=<.rbxl> resp: { "preview_hash": str, # для следующего шага "report": { "filename": str, "size_bytes": int, "version": int, "class_count": int, "instance_count": int, "top_classes": [{"class": str, "count": int}, ...], "scripts_total": int, "assets_to_download": int, "warnings": [str, ...] } } POST /import/rbxl/create body: { "preview_hash": str, "title": str, "auth_user_id": int } resp: { "project_id": int, "redirect": "/edit/" } — Парсит → конвертит → скачивает ассеты → создаёт запись в kubikon3d_projects. GET /health resp: { ok: true, version: "0.1.0" } Безопасность: эндпоинты доступны только МИНу (user_id=1). Проверка через X-User-Id header (NPM прокинет после JWT-проверки в user-сервисе). """ import os import sys import json import hashlib import tempfile import logging from pathlib import Path from flask import Flask, request, jsonify from flask_cors import CORS import psycopg2 from psycopg2.extras import RealDictCursor import redis sys.path.insert(0, os.path.dirname(__file__)) from rbxl_parser import parse, class_histogram from converter import Converter from asset_downloader import AssetDownloader, PendingDownload from mesh_converter import parse_roblox_mesh, build_glb, MeshParseError logging.basicConfig(level=logging.INFO) logger = logging.getLogger('rbxl-importer') PG_DSN = os.environ.get( 'PG_DSN', 'host=192.168.1.152 port=25435 user=min password=5cb157970ad5e3b2952ed05decaf1bab dbname=storys_db' ) REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0') STORAGE_ROOT = os.environ.get('STORAGE_ROOT', '/opt/roblox-assets') PUBLIC_ASSET_BASE = os.environ.get('PUBLIC_ASSET_BASE', 'https://assets.rublox.pro/roblox') MAX_RBXL_SIZE = 50 * 1024 * 1024 # 50 MB ALLOWED_USER_IDS = [1] # пока только МИН app = Flask(__name__) CORS(app, resources={r'/*': {'origins': '*'}}) # Devlog для удалённой отладки dev-сессий студии: фронт пушит сюда # console.error/warn, failed network requests, неожиданные exceptions. from app_devlog import devlog_bp app.register_blueprint(devlog_bp) try: rds = redis.from_url(REDIS_URL, decode_responses=False) rds.ping() logger.info(f'Redis connected: {REDIS_URL}') except Exception as e: logger.warning(f'Redis NOT connected: {e}; preview cache отключён') rds = None def pg_conn(): return psycopg2.connect(PG_DSN) def auth_check(req) -> int: """Возвращает user_id если ОК, иначе бросает RuntimeError.""" # X-User-Id выставляется upstream NPM или service-user после JWT-проверки. # В dev можно через header X-Auth-Override (только в LAN). user_id_str = req.headers.get('X-User-Id') or req.headers.get('X-Auth-Override') if not user_id_str: raise RuntimeError('No X-User-Id header') try: uid = int(user_id_str) except ValueError: raise RuntimeError(f'Bad X-User-Id: {user_id_str!r}') if uid not in ALLOWED_USER_IDS: raise RuntimeError(f'User {uid} not allowed (only МИН)') return uid @app.errorhandler(Exception) def on_error(e): logger.exception('handler error') return jsonify({'error': str(e), 'type': type(e).__name__}), 500 @app.get('/health') def health(): return jsonify({'ok': True, 'version': '0.1.0', 'service': 'rbxl-importer'}) @app.post('/import/rbxl/analyze') def analyze(): try: user_id = auth_check(request) except RuntimeError as e: return jsonify({'error': str(e)}), 403 if 'file' not in request.files: return jsonify({'error': 'file field required'}), 400 upload = request.files['file'] blob = upload.read() if len(blob) > MAX_RBXL_SIZE: return jsonify({'error': f'file too large (> {MAX_RBXL_SIZE} bytes)'}), 413 # Авто-детект XML vs Binary формата. # Бинарный: ... stripped = blob.lstrip() is_binary = stripped.startswith(b' None: """Walks project_data, заменяет {rbxAssetId: X} → url из asset_map.""" scene = project_data.get('scene', {}) for glb in scene.get('glbModels', []): rid = glb.get('rbxAssetId') if rid in asset_map: glb['url'] = asset_map[rid] for snd in scene.get('sounds', []): rid = snd.get('rbxAssetId') if rid in asset_map: snd['url'] = asset_map[rid] if __name__ == '__main__': app.run(host='0.0.0.0', port=8690, debug=False)