Files
pgames-manager/backend/app/converter/preflight.py
T

82 lines
4.3 KiB
Python

"""Read-only compatibility check, shared by uploads, CLI and the converter."""
from pathlib import Path
import tempfile
import zipfile
import shutil
import subprocess
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")