From af89e219cb8c993f8affec6321bcbdc40842fe68 Mon Sep 17 00:00:00 2001 From: Hipstercat Date: Wed, 22 Dec 2021 15:56:35 +0100 Subject: [PATCH] init --- .gitignore | 5 +++ app/__init__.py | 0 app/config.py | 7 ++++ app/database.py | 12 +++++++ app/dependencies.py | 10 ++++++ app/main.py | 15 ++++++++ app/models.py | 15 ++++++++ app/routers/__init__.py | 0 app/routers/maps.py | 79 +++++++++++++++++++++++++++++++++++++++++ app/schemas.py | 19 ++++++++++ app/utils.py | 17 +++++++++ config.json | 5 +++ requirements.txt | 6 ++++ test_upload_map.py | 24 +++++++++++++ 14 files changed, 214 insertions(+) create mode 100644 .gitignore create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/database.py create mode 100644 app/dependencies.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/maps.py create mode 100644 app/schemas.py create mode 100644 app/utils.py create mode 100644 config.json create mode 100644 requirements.txt create mode 100644 test_upload_map.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3be5330 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +blobs/ +database.sqlite +.idea/ +venv/ +*.pyc \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..0a8241e --- /dev/null +++ b/app/config.py @@ -0,0 +1,7 @@ +import json + +config = None + +with open("config.json", "r") as fp: + print("loading config") + config = json.load(fp) diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..c7f11c2 --- /dev/null +++ b/app/database.py @@ -0,0 +1,12 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./database.sqlite" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/app/dependencies.py b/app/dependencies.py new file mode 100644 index 0000000..541861d --- /dev/null +++ b/app/dependencies.py @@ -0,0 +1,10 @@ +from .database import SessionLocal + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..74b641a --- /dev/null +++ b/app/main.py @@ -0,0 +1,15 @@ +from fastapi import Depends, FastAPI +from .routers import maps +import app.models +from .database import engine +from fastapi.staticfiles import StaticFiles + + +app.models.Base.metadata.create_all(bind=engine) + +app = FastAPI( + title="Giants: Citizen Kabuto map API", + description="API to upload and download maps for Giants: Citizen Kabuto", +) +app.include_router(maps.router) +app.mount("/blobs", StaticFiles(directory="blobs"), name="static") diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..214945c --- /dev/null +++ b/app/models.py @@ -0,0 +1,15 @@ +import datetime +from sqlalchemy import Column, Integer, String, DateTime +# from sqlalchemy.orm import relationship + +from .database import Base + + +class Map(Base): + __tablename__ = "maps" + + crc = Column(Integer, primary_key=True, index=True) + name = Column(String) + size = Column(Integer) + upload_date = Column(DateTime, default=datetime.datetime.utcnow) + filename = Column(String) diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/maps.py b/app/routers/maps.py new file mode 100644 index 0000000..d8caaec --- /dev/null +++ b/app/routers/maps.py @@ -0,0 +1,79 @@ +import base64 +import struct +import io +import zipfile +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from ..dependencies import get_db +from ..schemas import * +from ..utils import crc32, map_model_to_out_schema +from ..models import * +from ..config import config + + +router = APIRouter() + +MAX_UPLOAD_SIZE = config["max_file_size"] + + +@router.get("/maps", tags=["maps"], response_model=List[MapOut]) +async def get_all_maps(db: Session = Depends(get_db)): + return [map_model_to_out_schema(m) for m in db.query(Map).all()] + + +@router.get("/map", tags=["maps"], response_model=MapOut) +async def get_map_by_crc(human_crc: Optional[str] = Query(None, min_length=8, max_length=8), + crc: Optional[int] = Query(None), + db: Session = Depends(get_db)): + + if not human_crc and not crc: + raise HTTPException(status_code=400, detail="Please use crc or human_crc but not both") + if human_crc and crc: + raise HTTPException(status_code=400, detail="Please use crc or human_crc but not both") + + if human_crc: + crc_int = struct.unpack(">L", bytes.fromhex(human_crc))[0] + else: + crc_int = crc + existing_map = db.query(Map).filter(Map.crc == crc_int).first() + if not existing_map: + raise HTTPException(status_code=404, detail="Map not found") + return map_model_to_out_schema(existing_map) + + +@router.post("/maps", tags=["maps"], response_model=MapOut) +async def upload_map(map_in: MapIn, db: Session = Depends(get_db)): + filename = map_in.name + if not filename.lower().endswith(".gck"): + raise HTTPException(status_code=400, detail="Invalid file") + + map_bytes = base64.b64decode(map_in.b64_data.encode("utf8")) + if len(map_bytes) > MAX_UPLOAD_SIZE: + raise HTTPException(status_code=400, detail="File too big") + + map_io = io.BytesIO(map_bytes) + try: + zipfile.ZipFile(map_io) + except zipfile.BadZipfile: + raise HTTPException(status_code=400, detail="File is not a valid map") + + crc = crc32(map_bytes) + existing_map = db.query(Map).filter(Map.crc == crc).first() + if existing_map: + return map_model_to_out_schema(existing_map) + else: + uploaded_filename = "%s.gck" % crc + with open("%s%s" % (config["upload_path"], uploaded_filename), "wb") as fp: + fp.write(map_bytes) + + uploaded_map = Map() + uploaded_map.crc = crc + uploaded_map.name = map_in.name + uploaded_map.filename = uploaded_filename + uploaded_map.size = len(map_bytes) + db.add(uploaded_map) + db.commit() + db.refresh(uploaded_map) + + return map_model_to_out_schema(uploaded_map) diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..2ccec08 --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,19 @@ +import datetime +from pydantic import BaseModel + + +class MapOut(BaseModel): + crc: int + crc_human: str + name: str + size: int + upload_date: datetime.datetime + blob_location: str + + class Config: + orm_mode = True + + +class MapIn(BaseModel): + name: str + b64_data: str diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..01d999d --- /dev/null +++ b/app/utils.py @@ -0,0 +1,17 @@ +import zlib +from .models import Map +from .schemas import MapOut +import copy +from .config import config + + +def crc32(bytes_in: bytes) -> int: + return zlib.crc32(bytes_in, 0) & 0xffffffff + + +def map_model_to_out_schema(map_in: Map) -> MapOut: + d = copy.deepcopy(map_in.__dict__) + d["crc_human"] = hex(map_in.crc)[2:].upper() + d["blob_location"] = "%s/%s%s" % (config["base_url"], config["upload_path"], map_in.filename) + map_out = MapOut(**d) + return map_out diff --git a/config.json b/config.json new file mode 100644 index 0000000..c7c24c0 --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "base_url": "https://gckmaps.hipstercat.fr", + "upload_path": "blobs/", + "max_file_size": 104857600 +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..32d734b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +fastapi~=0.65.1 +uvicorn +sqlalchemy~=1.4.15 +pydantic~=1.8.2 +requests +aiofiles \ No newline at end of file diff --git a/test_upload_map.py b/test_upload_map.py new file mode 100644 index 0000000..df8192c --- /dev/null +++ b/test_upload_map.py @@ -0,0 +1,24 @@ +import requests +import os +import base64 +import argparse + + +def read_file(path: str) -> bytes: + with open(path, "rb") as fp: + return fp.read() + + +def upload_map(map_path): + map_filename = os.path.basename(map_path) + content = read_file(map_path) + b64_content = base64.b64encode(content).decode("utf8") + r = requests.post("http://127.0.0.1:8000/maps", json={"name": map_filename, "b64_data": b64_content}) + print(r.json()) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("path") + args = parser.parse_args() + upload_map(args.path)