Files
pgames-manager/backend/app/converter/preflight.py
T
mrleo1nidandClaude Sonnet 5.5 bbbcc6a5d8 chore: add CI, lint config, login rate limit and split large modules
- Rate-limit failed logins (8 per 15 min per IP or username, 429 + Retry-After)
- Add ruff/mypy config and GitHub Actions workflow (ruff, pytest, tsc, tests, build)
- Apply ruff autofixes (import order, pyupgrade)
- Extract rpy_helpers.py, engine-util.ts, TranslationParts.tsx and
  translation-types.ts from oversized modules
- Add dependency-free React UI tests
- Refresh README and ARCHITECTURE (Naninovel/Tomie, status and plans)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:23:22 +03:00

82 lines
4.3 KiB
Python

"""Read-only compatibility check, shared by uploads, CLI and the converter."""
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from app.converter.archive import extract
from app.converter.compatibility import CompatibilityError, Issue, detect
def probe_archive(source: Path, filename: str) -> dict:
"""Cheap distribution check before queueing: list entries, never extract media."""
if zipfile.is_zipfile(source):
with zipfile.ZipFile(source) as archive:
entries = [entry.filename.replace("\\", "/") for entry in archive.infolist()]
else:
seven = shutil.which("7zz") or shutil.which("7z")
if not seven:
from app.converter.archive import ArchiveError
raise ArchiveError("Для проверки этого архива нужен 7-Zip")
try:
process = subprocess.run([seven, "l", "-slt", "-ba", "-sccUTF-8", str(source)],
capture_output=True, encoding="utf-8", errors="replace", timeout=120)
except subprocess.TimeoutExpired:
from app.converter.archive import ArchiveError
raise ArchiveError("Превышено время чтения списка файлов архива") from None
if process.returncode:
from app.converter.archive import ArchiveError
raise ArchiveError("Не удалось прочитать архив: " + (process.stderr or process.stdout)[-500:])
entries = [line[7:].replace("\\", "/") for line in process.stdout.splitlines() if line.startswith("Path = ")]
from app.converter.compatibility import IGNORED
entries = [entry.lower() for entry in entries
if not any(part.lower() in IGNORED for part in Path(entry).parts)]
engines = set()
if any("naninovel" in entry.rsplit("/", 1)[-1] and entry.endswith(".dll") for entry in entries):
engines.add("naninovel")
elif any("_data/managed/" in entry or "_data/il2cpp_data/" in entry or entry.endswith("_data/data.unity3d") for entry in entries):
engines.add("unity")
if any(entry.rsplit("/", 1)[-1] == "data.win" for entry in entries):
engines.add("gamemaker")
if any(entry.endswith((".pck", "/project.godot")) or entry == "project.godot" for entry in entries):
engines.add("godot")
if any(entry.endswith((".rgss3a", ".rgss2a", ".rgssad", "/rpg_core.js", "/rmmz_core.js")) for entry in entries):
engines.add("rpgmaker")
if any(entry.endswith((".rpy", ".rpyc", ".rpa")) and "renpy/" not in entry for entry in entries):
engines.add("renpy")
if len(engines) != 1:
raise CompatibilityError("ambiguous" if engines else "unknown", [Issue(
"multiple_engines" if engines else "unknown_engine",
"В архиве несколько движков: " + ", ".join(sorted(engines)) if engines else
"Движок не распознан; нужен полный дистрибутив игры")])
engine = engines.pop()
if engine not in {"renpy", "naninovel"}:
raise CompatibilityError(engine, [Issue("unsupported_engine", f"Движок {engine} пока не поддерживается")])
return {"engine": engine, "scriptsChecked": False}
def inspect_directory(root: Path, *, work: Path | None = None) -> dict:
detection = detect(root)
if detection.engine == "renpy":
from app.converter.analyze import analyze
report = analyze(detection.root)
issues = [Issue(**issue) for issue in report.pop("issues", [])]
if not report["hasStart"]:
issues.append(Issue("missing_entry", "Не найдена точка входа start"))
if issues:
raise CompatibilityError("renpy", issues)
return {"engine": "renpy", "compatible": True, **report}
from app.converter.naninovel import inspect_naninovel
return inspect_naninovel(detection.root, work=work)
def inspect_archive(source: Path, filename: str) -> dict:
if source.is_dir():
return inspect_directory(source)
with tempfile.TemporaryDirectory(prefix="pgames-preflight-") as directory:
root = Path(directory)
probe = probe_archive(source, filename)
extract(source, filename, root, only_scripts=probe["engine"] == "renpy")
return inspect_directory(root, work=root / "_inspection")