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.
This commit is contained in:
1 parent
06ff19ac4a
commit
c8d83b3fe7
65 files changed
+3825
-68
No files matched your search
@@ -1,11 +1,13 @@
|
||||
from typing import Annotated, Any
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from sqlalchemy import select
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.requests import ClientDisconnect
|
||||
|
||||
from app.deps import DB, CurrentUser, has_perm, require
|
||||
from app.models import Game, GameVersion, Save, User, VersionHistory
|
||||
from app.models import Game, GameVersion, Revision, Save, User, VersionHistory
|
||||
from app.routers.games import get_game
|
||||
from app.schemas import (
|
||||
ForkIn, HistoryOut, PublishIn, SaveDetailOut, SaveIn, SaveOut, VersionDetailOut, VersionIn,
|
||||
@@ -77,8 +79,20 @@ async def upload_version(
|
||||
await db.commit()
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Пустой файл")
|
||||
|
||||
from app.converter.preflight import probe_archive
|
||||
from app.converter.archive import ArchiveError
|
||||
from app.converter.compatibility import CompatibilityError
|
||||
try:
|
||||
probe = await asyncio.to_thread(probe_archive, path, filename)
|
||||
except (CompatibilityError, ArchiveError, OSError, ValueError) as exc:
|
||||
remove_upload(version.id)
|
||||
await db.delete(version)
|
||||
await db.commit()
|
||||
raise HTTPException(422, str(exc)) from None
|
||||
|
||||
version.status = "queued"
|
||||
version.progress = "В очереди на конвертацию"
|
||||
version.progress = "В очереди на проверку совместимости"
|
||||
version.report = {"engine": probe["engine"], "compatible": None}
|
||||
version.size_bytes = size
|
||||
await db.commit()
|
||||
return version
|
||||
@@ -94,7 +108,7 @@ async def version_manifest(version_id: int, user: Player, db: DB) -> dict[str, A
|
||||
v = await get_version(db, user, version_id)
|
||||
if v.status != "ready" or not v.manifest:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Версия ещё не готова")
|
||||
return v.manifest
|
||||
return {**v.manifest, "revision": v.revision_id, "versionId": v.id}
|
||||
|
||||
|
||||
@router.post("/versions/{version_id}/fork", response_model=VersionOut, status_code=201)
|
||||
@@ -117,9 +131,11 @@ async def fork_version(version_id: int, body: ForkIn,
|
||||
visibility="draft",
|
||||
kind=body.kind,
|
||||
manifest=parent.manifest,
|
||||
base_revision_id=parent.revision_id,
|
||||
report=parent.report,
|
||||
size_bytes=parent.size_bytes,
|
||||
)
|
||||
fork._revision_parents = [parent.revision_id] if parent.revision_id else []
|
||||
db.add(fork)
|
||||
await db.commit()
|
||||
return fork
|
||||
@@ -147,16 +163,26 @@ async def publish_version(version_id: int, body: PublishIn, user: Publisher, db:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "Версия ещё не готова")
|
||||
game = await db.get(Game, v.game_id)
|
||||
if v.kind == "translation" and game.main_version_id and game.main_version_id != v.id:
|
||||
main = await lock_version(db, game.main_version_id)
|
||||
parent = await db.get(GameVersion, v.parent_id) if v.parent_id else None
|
||||
langs = translation.changed_languages(v.manifest, parent.manifest if parent else None)
|
||||
locked = {key: await lock_version(db, key) for key in sorted((v.id, game.main_version_id))}
|
||||
v, main = locked[v.id], locked[game.main_version_id]
|
||||
base = await db.get(Revision, v.base_revision_id) if v.base_revision_id else None
|
||||
if base is None:
|
||||
raise HTTPException(409, "Историческая база перевода неизвестна. Создайте новый форк перевода")
|
||||
langs = translation.changed_languages(v.manifest, base.manifest)
|
||||
if not langs:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "В версии нет изменённых переводов")
|
||||
merged, conflicts = await run_in_threadpool(translation.merge_translation, base.manifest, v.manifest, main.manifest)
|
||||
if conflicts:
|
||||
raise HTTPException(409, "Конфликт перевода; согласуйте изменения строк: " + ", ".join(c["path"] for c in conflicts[:10]))
|
||||
db.add(VersionHistory(version_id=main.id, user_id=user.id, manifest=main.manifest,
|
||||
note=f"Перевод из «{v.name}»: {', '.join(langs)}"))
|
||||
main.manifest = translation.merge_languages(main.manifest, v.manifest, langs)
|
||||
elif body.make_main:
|
||||
game.main_version_id = v.id
|
||||
main._revision_parents = [main.revision_id, v.revision_id]
|
||||
main.manifest = merged
|
||||
main.size_bytes = sum(f["size"] for f in merged["files"].values())
|
||||
else:
|
||||
v = await lock_version(db, v.id)
|
||||
if body.make_main:
|
||||
game.main_version_id = v.id
|
||||
v.visibility = "published"
|
||||
await db.commit()
|
||||
await db.refresh(v)
|
||||
|
||||
Reference in new issue
Block a user