Files
pgames-manager/backend/app/import_library.py
T
mrleo1nid c8d83b3fe7 feat: add mod editing, revision merges and secure game storage
Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
2026-10-25 04:42:48 +03:00

101 lines
4.6 KiB
Python

"""Check/import a local collection. Rejected games never get queued.
python -m app.import_library /games --check
python -m app.import_library /games --import --owner admin
"""
import argparse
import asyncio
import json
from pathlib import Path
import tempfile
import sys
from app.converter.archive import ArchiveError, extract
from app.converter.compatibility import CompatibilityError, Issue
from app.converter.preflight import inspect_archive, inspect_directory
ARCHIVES = (".zip", ".7z", ".rar", ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tar.xz")
def entries(root: Path):
"""An unpacked copy takes precedence over an archive with the identical name."""
directories = {p.name.casefold() for p in root.iterdir() if p.is_dir() and not p.name.startswith(".")}
for source in sorted(root.iterdir(), key=lambda p: p.name.casefold()):
if source.name.startswith(".") or source.is_symlink():
continue
suffix = next((ext for ext in ARCHIVES if source.name.lower().endswith(ext)), None)
if source.is_file() and suffix and source.name[:-len(suffix)].casefold() in directories:
continue
yield source
def check(source: Path) -> dict:
try:
if source.is_dir():
return inspect_directory(source)
if not source.name.lower().endswith(ARCHIVES):
raise CompatibilityError("unknown", [Issue("not_distribution",
"Нужна папка или архив полного дистрибутива; отдельные EXE и RPA не импортируются")])
return inspect_archive(source, source.name)
except CompatibilityError as exc:
return exc.report()
except Exception as exc:
return {"compatible": False, "engine": "unknown", "error": str(exc),
"issues": [{"code": "input_error", "message": str(exc), "location": source.name, "count": 1}]}
async def import_one(source: Path, report: dict, username: str | None) -> dict:
from sqlalchemy import select
from app.db import SessionLocal
from app.models import Game, GameVersion, Role, User
from app.worker import convert
async with SessionLocal() as db:
stmt = select(User).join(Role).order_by(User.id)
stmt = stmt.where(User.username == username) if username else stmt.where(Role.name == "admin")
owner = await db.scalar(stmt.limit(1))
if owner is None:
raise ValueError("Пользователь для импорта не найден")
game = Game(title=source.name, engine=report["engine"], created_by_id=owner.id)
db.add(game)
await db.flush()
version = GameVersion(game_id=game.id, owner_id=owner.id, name="Оригинал",
upload_name=source.name, status="importing", report=report)
db.add(version)
await db.commit()
game_id, version_id = game.id, version.id
ok = await convert(version_id, source, source.name)
return {"gameId": game_id, "versionId": version_id, "status": "ready" if ok else "failed"}
async def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("root", type=Path)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--check", action="store_true", help="Только проверка; без изменения библиотеки")
mode.add_argument("--import", dest="import_games", action="store_true", help="Импортировать совместимые игры")
parser.add_argument("--owner")
parser.add_argument("--output", type=Path, help="Записать полный JSON-отчёт")
args = parser.parse_args()
if not args.root.is_dir():
parser.error("root должен быть каталогом коллекции")
results = []
for source in entries(args.root):
print(json.dumps({"game": source.name, "status": "checking"}, ensure_ascii=False), flush=True)
report = await asyncio.to_thread(check, source)
result = {"game": source.name, **report}
if args.import_games and report.get("compatible"):
result.update(await import_one(source, report, args.owner))
results.append(result)
print(json.dumps(result, ensure_ascii=False), flush=True)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
if any(not result.get("compatible") or result.get("status") == "failed" for result in results):
raise SystemExit(2)
if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8")
asyncio.run(main())