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
@@ -0,0 +1,100 @@
|
||||
"""Disposable local UI fixture. Run seed/cleanup only on the pgames-test stand."""
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import delete
|
||||
|
||||
from app.config import get_settings
|
||||
from app.converter.pipeline import convert_archive
|
||||
from app.db import SessionLocal
|
||||
from app.maintenance import storage_access
|
||||
from app.models import Game, GameVersion, Role, User
|
||||
from app.security import hash_password
|
||||
from tests.make_sample import build
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=("seed", "check", "cleanup"))
|
||||
args = parser.parse_args()
|
||||
record = get_settings().data_dir / "mod-ui-fixture.json"
|
||||
if args.action == "check":
|
||||
import http.cookiejar
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
data = json.loads(record.read_text())
|
||||
host = "http://web"
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
||||
payload = json.dumps({"username": data["username"], "password": data["password"]}).encode()
|
||||
opener.open(urllib.request.Request(host + "/api/auth/login", data=payload, headers={"Content-Type": "application/json"}))
|
||||
manifest = json.load(opener.open(host + f'/api/versions/{data["version_id"]}/manifest'))
|
||||
key = manifest["files"]["game.json"]["hash"]
|
||||
file_url = host + f'/api/versions/{data["version_id"]}/files/{key}'
|
||||
response = opener.open(file_url)
|
||||
assert response.status == 200 and "private" in response.headers["Cache-Control"]
|
||||
assert json.load(response)["format"] == "pgvn/1"
|
||||
assert opener.open(urllib.request.Request(file_url, method="HEAD")).status == 200
|
||||
audio = next(f["hash"] for p, f in manifest["files"].items() if p.endswith(".ogg"))
|
||||
response = opener.open(urllib.request.Request(host + f'/api/versions/{data["version_id"]}/files/{audio}', headers={"Range": "bytes=0-9"}))
|
||||
assert response.status == 206 and len(response.read()) == 10
|
||||
for url, status in [(file_url, 401), (host + f"/_protected_blobs/{key[:2]}/{key[2:4]}/{key}", 404)]:
|
||||
try:
|
||||
urllib.request.urlopen(url)
|
||||
except urllib.error.HTTPError as error:
|
||||
assert error.code == status
|
||||
else:
|
||||
raise AssertionError("Unprotected media route")
|
||||
print("nginx: authenticated JSON/HEAD/Range OK; anonymous and direct internal access denied")
|
||||
return
|
||||
async with storage_access():
|
||||
async with SessionLocal() as db:
|
||||
if args.action == "cleanup":
|
||||
if not record.exists():
|
||||
return
|
||||
data = json.loads(record.read_text())
|
||||
user = await db.get(User, data["user_id"])
|
||||
if not user or user.username != data["username"] or not user.username.startswith("mod-ui-"):
|
||||
raise RuntimeError("Fixture identity mismatch; cleanup aborted")
|
||||
await db.execute(delete(Game).where(Game.created_by_id == user.id))
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
await db.execute(delete(Role).where(Role.id == data["role_id"]))
|
||||
await db.commit()
|
||||
record.unlink()
|
||||
print("Disposable UI user and games removed")
|
||||
return
|
||||
if record.exists():
|
||||
print(record.read_text())
|
||||
return
|
||||
suffix = uuid4().hex[:10]
|
||||
password = uuid4().hex
|
||||
role = Role(name="mod-ui-" + suffix, permissions=["games.play", "versions.fork", "games.upload", "games.edit", "versions.publish"])
|
||||
db.add(role)
|
||||
await db.flush()
|
||||
user = User(username="mod-ui-" + suffix, password_hash=hash_password(password), role_id=role.id, must_change_password=False)
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root = Path(folder)
|
||||
result = await asyncio.to_thread(convert_archive, build(root / "sample.zip"), "sample.zip", root / "work", lambda _: None)
|
||||
game = Game(title="Проверка редактора модов", created_by_id=user.id)
|
||||
db.add(game)
|
||||
await db.flush()
|
||||
original = GameVersion(game_id=game.id, owner_id=user.id, name="Оригинал для проверки", status="ready", manifest=result.manifest)
|
||||
db.add(original)
|
||||
await db.flush()
|
||||
fork = GameVersion(game_id=game.id, parent_id=original.id, owner_id=user.id, name="Мой тестовый мод", kind="mod", visibility="draft", status="ready", manifest=result.manifest, base_revision_id=original.revision_id)
|
||||
fork._revision_parents = [original.revision_id]
|
||||
db.add(fork)
|
||||
await db.commit()
|
||||
data = dict(username=user.username, password=password, user_id=user.id, role_id=role.id, version_id=fork.id, game_id=game.id)
|
||||
record.write_text(json.dumps(data))
|
||||
print(json.dumps(data))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in new issue
Block a user