MongoDB migration

This commit is contained in:
Profitroll 2023-01-18 14:25:22 +01:00
parent 94f8839e53
commit e9e9e3784a
19 changed files with 462 additions and 138 deletions

View File

@ -1,4 +1,11 @@
{ {
"database": {
"name": "stardew_sync",
"host": "127.0.0.1",
"port": 27017,
"user": null,
"password": null
},
"locations": { "locations": {
"data": "data" "data": "data"
}, },

View File

@ -1 +0,0 @@
[]

View File

@ -1 +0,0 @@
[]

View File

@ -1,9 +1,8 @@
from os import path
from uuid import uuid4 from uuid import uuid4
from shutil import move
from models.apikey import APIKeyUpdated from models.apikey import APIKeyUpdated
from modules.app import app, get_api_key from modules.app import app, get_api_key
from modules.utils import configGet, jsonLoad, jsonSave from modules.security import passEncode
from modules.database import col_apikeys, col_expired
from fastapi import Depends from fastapi import Depends
from fastapi.responses import UJSONResponse from fastapi.responses import UJSONResponse
from fastapi.openapi.models import APIKey from fastapi.openapi.models import APIKey
@ -11,21 +10,9 @@ from fastapi.openapi.models import APIKey
@app.put("/apikey", response_class=UJSONResponse, response_model=APIKeyUpdated, description="Update API key") @app.put("/apikey", response_class=UJSONResponse, response_model=APIKeyUpdated, description="Update API key")
async def apikey_put(apikey: APIKey = Depends(get_api_key)): async def apikey_put(apikey: APIKey = Depends(get_api_key)):
keys_valid = jsonLoad(path.join(configGet("data", "locations"), "api_keys.json"))
keys_expired = jsonLoad(path.join(configGet("data", "locations"), "expired_keys.json"))
new_key = str(uuid4()) new_key = str(uuid4())
col_apikeys.find_one_and_replace({"hash": passEncode(apikey)}, {"hash": passEncode(new_key)})
keys_valid.remove(apikey) col_expired.insert_one({"hash": passEncode(apikey)})
keys_valid.append(new_key)
keys_expired.append(apikey)
jsonSave(keys_valid, path.join(configGet("data", "locations"), "api_keys.json"))
jsonSave(keys_expired, path.join(configGet("data", "locations"), "expired_keys.json"))
if path.exists(path.join(configGet("data", "locations"), apikey)): # type: ignore
move(path.join(configGet("data", "locations"), apikey), path.join(configGet("data", "locations"), new_key)) # type: ignore
return UJSONResponse({"apikey": new_key}) return UJSONResponse({"apikey": new_key})

View File

@ -1,21 +1,72 @@
# from modules.app import app, get_api_key from modules.app import app, get_api_key, user_by_key
# from modules.utils import configGet, jsonLoad from modules.database import col_devices, col_saves
# from fastapi import HTTPException, Depends from fastapi import HTTPException, Depends
# from fastapi.responses import UJSONResponse, FileResponse from fastapi.responses import UJSONResponse, Response
# from fastapi.openapi.models import APIKey from fastapi.openapi.models import APIKey
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
# @app.get("/devices", response_class=UJSONResponse, description="Get all devices") @app.get("/devices", response_class=UJSONResponse, description="Get all devices")
# async def devices_get(apikey: APIKey = Depends(get_api_key)): async def devices_get(apikey: APIKey = Depends(get_api_key)):
# pass
# @app.get("/devices/{name}", response_class=UJSONResponse, description="Get game saves from device by name") devices = list(col_devices.find({"user": user_by_key(apikey)}))
# async def devices_get_by_name(name: str, apikey: APIKey = Depends(get_api_key)):
# pass
# @app.post("/devices", response_class=UJSONResponse, description="Create new device") if len(devices) == 0:
# async def devices_post(name: str, apikey: APIKey = Depends(get_api_key)): raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any devices.")
# pass
# @app.put("/devices/{name}", response_class=UJSONResponse, description="Update name of the existing device") output = []
# async def devices_put(name: str, apikey: APIKey = Depends(get_api_key)):
# pass for device in devices:
out_device = device
del out_device["_id"]
del out_device["user"]
output.append(out_device)
return UJSONResponse(output)
@app.get("/devices/{name}", response_class=UJSONResponse, description="Get information about device by name")
async def devices_get_by_name(name: str, apikey: APIKey = Depends(get_api_key)):
device = col_devices.find_one({"user": user_by_key(apikey), "name": name})
if device is not None:
del device["_id"]
return UJSONResponse(device)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
@app.delete("/devices/{name}", description="Get information about device by name")
async def devices_delete_by_name(name: str, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
device = col_devices.find_one({"user": user, "name": name})
if device is not None:
col_devices.find_one_and_delete({"user": user, "name": name})
col_saves.delete_many({"user": user, "device": name})
return Response(status_code=HTTP_204_NO_CONTENT)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
@app.post("/devices", response_class=UJSONResponse, description="Create new device")
async def devices_post(name: str, os: str, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
if col_devices.find_one({"user": user, "name": name}) is not None:
raise HTTPException(HTTP_409_CONFLICT, detail="Device with this name already exists.")
col_devices.insert_one({"user": user, "name": name, "os": os, "last_save": 0})
return Response(status_code=HTTP_204_NO_CONTENT)
@app.patch("/devices/{name}", description="Update name of the existing device")
async def devices_patch(name: str, new_name: str, os: str, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
if col_devices.find_one({"user": user, "name": name}) is None:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
if col_devices.find_one({"user": user, "name": new_name}) is not None:
raise HTTPException(HTTP_409_CONFLICT, detail="Device with this name already exists.")
col_devices.find_one_and_update({"user": user, "name": name}, {"$set": {"name": new_name, "os": os}})
col_saves.update_many({"user": user, "device": name}, {"$set": {"device": new_name}})
return Response(status_code=HTTP_204_NO_CONTENT)

View File

@ -1,12 +1,14 @@
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from os import listdir, makedirs, path, rmdir, sep from urllib.parse import quote_plus
from typing import Dict, List from os import path, remove
from typing import Dict, List, Union
from zipfile import ZipFile from zipfile import ZipFile
from xmltodict import parse from xmltodict import parse
from models.saves import StardewSave from models.saves import StardewSave
from modules.app import app, get_api_key from modules.app import app, get_api_key, user_by_key
from modules.utils import configGet, jsonLoad, jsonSave from modules.utils import saveFile
from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends, UploadFile from fastapi import HTTPException, Depends, UploadFile
from fastapi.responses import UJSONResponse, FileResponse, Response from fastapi.responses import UJSONResponse, FileResponse, Response
from fastapi.openapi.models import APIKey from fastapi.openapi.models import APIKey
@ -26,7 +28,9 @@ def zipfiles(filenames, save_name: str) -> Response:
fdir, fname = path.split(fpath) fdir, fname = path.split(fpath)
# Add file, at correct path # Add file, at correct path
zf.write(fpath, fname) for entry in (list(col_saves.find({"files.save.uuid": fname})) + list(col_saves.find({"files.saveinfo.uuid": fname}))):
filename = entry["files"]["save"]["name"] if (entry["files"]["save"]["uuid"] == fname) else entry["files"]["saveinfo"]["name"]
zf.write(fpath, filename)
# Must close zip for all contents to be written # Must close zip for all contents to be written
zf.close() zf.close()
@ -36,78 +40,92 @@ def zipfiles(filenames, save_name: str) -> Response:
s.getvalue(), s.getvalue(),
media_type="application/x-zip-compressed", media_type="application/x-zip-compressed",
headers={ headers={
'Content-Disposition': f'attachment;filename={zip_filename}' 'Content-Disposition': f'attachment;filename={quote_plus(zip_filename)}'
} }
) )
@app.get("/saves", response_class=UJSONResponse, response_model=Dict[str, StardewSave], description="Get all available game saves") @app.get("/saves", response_class=UJSONResponse, response_model=Dict[str, StardewSave], description="Get all available game saves")
async def saves_get(apikey: APIKey = Depends(get_api_key)): async def saves_get(apikey: APIKey = Depends(get_api_key)):
save_path = path.join(configGet("data", "locations"), "users", apikey) # type: ignore
if path.exists(save_path): saves_entries = list(col_saves.find({"user": user_by_key(apikey)}))
output = {}
for id in listdir(save_path): if len(saves_entries) == 0:
print("Iterating through", save_path, "on", id)
for dir in listdir(path.join(save_path, id)):
print("Iterating through", path.join(save_path, id, dir), "on", dir)
d = path.join(save_path, id, dir)
if path.isdir(d):
if str(id) not in output:
output[str(id)] = []
output[str(id)].append(jsonLoad(path.join(configGet("data", "locations"), "users", apikey, id, dir, "index.json"))) # type: ignore
return UJSONResponse(output)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any saves.") raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any saves.")
output = []
for entry in saves_entries:
out_entry = entry
del out_entry["_id"]
del out_entry["user"]
del out_entry["files"]
output.append(out_entry)
return UJSONResponse(output)
@app.get("/saves/{id}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name") @app.get("/saves/{id}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
async def saves_get_by_id(id: str, apikey: APIKey = Depends(get_api_key)): async def saves_get_by_id(id: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
save_path = path.join(configGet("data", "locations"), "users", apikey, id) # type: ignore
if path.exists(save_path): saves_entries = list(col_saves.find({"id": id})) if device is None else list(col_saves.find({"id": id, "device": device}))
output = []
for dir in listdir(save_path): if len(saves_entries) == 0:
d = path.join(save_path, dir) raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
if path.isdir(d):
output.append(jsonLoad(path.join(configGet("data", "locations"), "users", apikey, id, dir, "index.json"))) # type: ignore output = []
return UJSONResponse(output)
for entry in saves_entries:
out_entry = entry
del out_entry["_id"]
del out_entry["files"]
del out_entry["user"]
output.append(out_entry)
return UJSONResponse(output)
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
async def saves_get_by_both_ids(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date})
if saves_entry is not None:
del saves_entry["_id"]
del saves_entry["files"]
return UJSONResponse(saves_entry)
else: else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.") raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.delete("/saves/{id}", description="Get game saves by name") @app.delete("/saves/{id}", description="Get game saves by name")
async def saves_delete_by_id(id: str, apikey: APIKey = Depends(get_api_key)): async def saves_delete_by_id(id: int, apikey: APIKey = Depends(get_api_key)):
save_path = path.join(configGet("data", "locations"), "users", apikey, id) # type: ignore user = user_by_key(apikey)
if path.exists(save_path): if col_saves.count_documents({"user": user, "id": id}) > 0:
rmdir(save_path) saves_entries = list(col_saves.find({"user": user, "id": id}))
for entry in saves_entries:
remove(entry["files"]["save"]["path"])
remove(entry["files"]["saveinfo"]["path"])
col_saves.delete_many({"user": user, "id": id})
return Response(status_code=HTTP_204_NO_CONTENT) return Response(status_code=HTTP_204_NO_CONTENT)
else: else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.") raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name") @app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, description="Get game saves by name")
async def saves_delete_by_both_ids(id: str, save_date: str, apikey: APIKey = Depends(get_api_key)): async def saves_delete_by_both_ids(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
save_path = path.join(configGet("data", "locations"), "users", apikey, id, save_date) # type: ignore saves_entry = col_saves.find_one_and_delete({"id": id, "date": save_date})
if path.exists(save_path): if saves_entry is not None:
rmdir(save_path) remove(saves_entry["files"]["save"]["path"])
remove(saves_entry["files"]["saveinfo"]["path"])
return Response(status_code=HTTP_204_NO_CONTENT) return Response(status_code=HTTP_204_NO_CONTENT)
else: else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.") raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
async def saves_get_by_both_ids(id: str, save_date: str, apikey: APIKey = Depends(get_api_key)):
save_path = path.join(configGet("data", "locations"), "users", apikey, id, save_date) # type: ignore
if path.exists(save_path):
return UJSONResponse(jsonLoad(save_path+sep+"index.json"))
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.get("/saves/{id}/{save_date}/download", response_class=FileResponse, description="Get game save as .svsave file by its id and save date") @app.get("/saves/{id}/{save_date}/download", response_class=FileResponse, description="Get game save as .svsave file by its id and save date")
async def saves_download(id: str, save_date: str, apikey: APIKey = Depends(get_api_key)): async def saves_download(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
if path.exists(path.join(configGet("data", "locations"), "users", apikey, id, save_date)): # type: ignore saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date})
save_path = path.join(configGet("data", "locations"), "users", apikey, id, save_date) # type: ignore if saves_entry is not None: # type: ignore
return zipfiles([f"{save_path}{sep}{id}", f"{save_path}{sep}SaveGameInfo", f"{save_path}{sep}index.json"], save_name=f"{id}_{save_date}") return zipfiles([saves_entry["files"]["save"]["path"], saves_entry["files"]["saveinfo"]["path"]], save_name=f'{saves_entry["data"]["farmer"]}_{saves_entry["id"]}')
else: else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.") raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@ -115,6 +133,8 @@ async def saves_download(id: str, save_date: str, apikey: APIKey = Depends(get_a
@app.post("/saves", response_class=UJSONResponse, response_model=StardewSave, description="Upload new save") @app.post("/saves", response_class=UJSONResponse, response_model=StardewSave, description="Upload new save")
async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depends(get_api_key)): async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
error_return = HTTPException(HTTP_406_NOT_ACCEPTABLE, detail="You must provide two files: save file and SaveGameInfo for that save") error_return = HTTPException(HTTP_406_NOT_ACCEPTABLE, detail="You must provide two files: save file and SaveGameInfo for that save")
if len(files) != 2: if len(files) != 2:
@ -123,48 +143,67 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
if "SaveGameInfo" not in [file.filename for file in files]: if "SaveGameInfo" not in [file.filename for file in files]:
return error_return return error_return
save_info = save_data = save_info_file = save_data_file = None if col_devices.find_one({"user": user, "name": device}) is None:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
save_info = save_data = save_info_file = save_data_filename = save_data_file = save_info_file_id = save_data_file_id = None
for file in files: for file in files:
if file.filename == "SaveGameInfo": if file.filename == "SaveGameInfo":
save_info_file = await file.read() save_info_file = await file.read()
save_info_file_id = saveFile(save_info_file)
save_info = parse(save_info_file.decode("utf-8")) save_info = parse(save_info_file.decode("utf-8"))
if "Farmer" not in save_info: if "Farmer" not in save_info:
return error_return return error_return
else: else:
save_data_filename = file.filename
save_data_file = await file.read() save_data_file = await file.read()
save_data_file_id = saveFile(save_data_file)
save_data = parse(save_data_file.decode("utf-8")) save_data = parse(save_data_file.decode("utf-8"))
if "SaveGame" not in save_data: if "SaveGame" not in save_data:
return error_return return error_return
if save_info is None or save_data is None or save_info_file is None or save_data_file is None: if save_info is None or save_data is None or save_info_file is None or save_data_filename is None or save_data_file is None or save_info_file_id is None or save_data_file_id is None:
return error_return return error_return
now = datetime.now() save_date = int(datetime.utcnow().timestamp())
save_date = now.strftime("%Y%m%d_%H%M%S")
makedirs(path.join(configGet("data", "locations"), "users", apikey, save_data["SaveGame"]["uniqueIDForThisGame"], save_date), exist_ok=True) # type: ignore
with open(path.join(configGet("data", "locations"), "users", apikey, save_data["SaveGame"]["uniqueIDForThisGame"], save_date, "SaveGameInfo"), "wb") as f: # type: ignore
f.write(save_info_file)
with open(path.join(configGet("data", "locations"), "users", apikey, save_data["SaveGame"]["uniqueIDForThisGame"], save_date, save_data["SaveGame"]["uniqueIDForThisGame"]), "wb") as f: # type: ignore
f.write(save_data_file)
index = { index = {
"id": int(save_data["SaveGame"]["uniqueIDForThisGame"]), "id": int(save_data["SaveGame"]["uniqueIDForThisGame"]),
"user": user,
"device": device, "device": device,
"farmer": save_info["Farmer"]["name"], "date": save_date,
"year": int(save_info["Farmer"]["yearForSaveGame"]), "data": {
"season": int(save_info["Farmer"]["seasonForSaveGame"]), "farmer": save_info["Farmer"]["name"],
"day": int(save_info["Farmer"]["dayOfMonthForSaveGame"]), "money": int(save_info["Farmer"]["money"]),
"money": int(save_info["Farmer"]["money"]), "played": int(save_info["Farmer"]["millisecondsPlayed"]),
"played": int(save_info["Farmer"]["millisecondsPlayed"]), "save_time": int(save_info["Farmer"]["saveTime"]),
"save_time": int(save_info["Farmer"]["saveTime"]), "year": int(save_info["Farmer"]["yearForSaveGame"]),
"save_date": save_date, "season": int(save_info["Farmer"]["seasonForSaveGame"]),
"uploaded": now.isoformat() "day": int(save_info["Farmer"]["dayOfMonthForSaveGame"])
},
"files": {
"save": {
"name": save_data_filename,
"uuid": save_data_file_id[0],
"path": save_data_file_id[1]
},
"saveinfo": {
"name": "SaveGameInfo",
"uuid": save_info_file_id[0],
"path": save_info_file_id[1]
}
}
} }
col_saves.insert_one(index)
jsonSave(index, path.join(configGet("data", "locations"), "users", apikey, save_data["SaveGame"]["uniqueIDForThisGame"], save_date, "index.json")) # type: ignore del save_info, save_data
del index["user"]
del index["files"]
del index["_id"]
col_devices.find_one_and_update({"user": user, "name": device}, {"$set": {"last_save": save_date}})
return UJSONResponse(index) return UJSONResponse(index)

View File

@ -1 +1,7 @@
# from pydantic import BaseModel from pydantic import BaseModel
class Device(BaseModel):
user: str
name: str
os: str
last_save: int

View File

@ -1,14 +1,16 @@
from pydantic import BaseModel from pydantic import BaseModel
class StardewSaveData(BaseModel):
farmer: str
money: int
played: int
save_time: int
year: int
season: int
day: int
class StardewSave(BaseModel): class StardewSave(BaseModel):
id: int id: int
device: str device: str
farmer: str date: int
year: int data: StardewSaveData
season: int
day: int
money: int
played: int
save_time: int
save_date: str
uploaded: str

View File

@ -1,11 +1,14 @@
from os import path from os import path
from typing import Union
from fastapi import FastAPI, Security, HTTPException from fastapi import FastAPI, Security, HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
from fastapi.security import APIKeyQuery, APIKeyHeader, APIKeyCookie from fastapi.security import APIKeyQuery, APIKeyHeader, APIKeyCookie
from fastapi.openapi.models import APIKey
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
from starlette.status import HTTP_401_UNAUTHORIZED from starlette.status import HTTP_401_UNAUTHORIZED
from modules.utils import configGet, jsonLoad from modules.utils import configGet, jsonLoad
from modules.security import passEncode
from modules.database import col_apikeys, col_expired
app = FastAPI(title="Stardew Sync", docs_url=None, redoc_url=None, version="0.1") app = FastAPI(title="Stardew Sync", docs_url=None, redoc_url=None, version="0.1")
@ -40,11 +43,8 @@ async def get_api_key(
api_key_cookie: str = Security(api_key_cookie), api_key_cookie: str = Security(api_key_cookie),
) -> str: ) -> str:
keys = get_all_api_keys()
expired = get_all_expired_keys()
def is_valid(key): def is_valid(key):
return True if key in keys else False return True if col_apikeys.find_one({"hash": passEncode(key)}) is not None else False
if is_valid(api_key_query): if is_valid(api_key_query):
return api_key_query return api_key_query
@ -53,11 +53,15 @@ async def get_api_key(
elif is_valid(api_key_cookie): elif is_valid(api_key_cookie):
return api_key_cookie return api_key_cookie
else: else:
if (api_key_query in expired) or (api_key_header in expired) or (api_key_cookie in expired): if (col_expired.find_one({"hash": passEncode(api_key_query)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_header)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_cookie)}) is not None):
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=configGet("key_expired", "messages")) raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=configGet("key_expired", "messages"))
else: else:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages"))
def user_by_key(apikey: Union[str, APIKey]) -> Union[str, None]:
db_key = col_apikeys.find_one({"hash": passEncode(apikey)})
return db_key["user"] if db_key is not None else None
@app.get("/docs", include_in_schema=False) @app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html(): async def custom_swagger_ui_html():
return get_swagger_ui_html( return get_swagger_ui_html(

34
modules/database.py Normal file
View File

@ -0,0 +1,34 @@
from modules.utils import configGet
from pymongo import MongoClient
db_config = configGet("database")
if db_config["user"] is not None and db_config["password"] is not None:
con_string = 'mongodb://{0}:{1}@{2}:{3}/{4}'.format(
db_config["user"],
db_config["password"],
db_config["host"],
db_config["port"],
db_config["name"]
)
else:
con_string = 'mongodb://{0}:{1}/{2}'.format(
db_config["host"],
db_config["port"],
db_config["name"]
)
db_client = MongoClient(con_string)
db = db_client.get_database(name=db_config["name"])
collections = db.list_collection_names()
for collection in ["saves", "devices", "apikeys", "expired"]:
if not collection in collections:
db.create_collection(collection)
col_saves = db.get_collection("saves")
col_devices = db.get_collection("devices")
col_apikeys = db.get_collection("apikeys")
col_expired = db.get_collection("expired")

17
modules/security.py Normal file
View File

@ -0,0 +1,17 @@
from hashlib import pbkdf2_hmac
from os import chmod, path, urandom
from typing import Union
from modules.utils import configGet
from fastapi.openapi.models import APIKey
def saltRead():
if not path.exists(path.join(configGet("data", "locations"), "salt")):
with open(path.join(configGet("data", "locations"), "salt"), "wb") as file:
file.write(urandom(32))
chmod(path.join(configGet("data", "locations"), "salt"), mode=0o600)
with open(path.join(configGet("data", "locations"), "salt"), "rb") as file:
contents = file.read()
return contents
def passEncode(password: Union[str, APIKey, None]) -> Union[bytes, None]:
return None if password is None else pbkdf2_hmac("sha256", str(password).encode("utf-8"), saltRead(), 96800, dklen=128)

View File

@ -1,4 +1,6 @@
from typing import Any, Union from os import makedirs, path
from typing import Any, Tuple, Union
from uuid import uuid4
from ujson import loads, dumps, JSONDecodeError from ujson import loads, dumps, JSONDecodeError
from traceback import print_exc from traceback import print_exc
@ -58,4 +60,19 @@ def configGet(key: str, *args: str) -> Any:
this_key = this_dict this_key = this_dict
for dict_key in args: for dict_key in args:
this_key = this_key[dict_key] this_key = this_key[dict_key]
return this_key[key] return this_key[key]
def saveFile(filebytes: bytes) -> Tuple[str, str]:
"""Save some bytedata into random file and return its ID
### Args:
* filebytes (`bytes`): Bytes to write into file
### Returns:
* `Tuple[str, str]`: Tuple where first item is an ID and the second is an absolute path to file
"""
makedirs(path.join(configGet("data", "locations"), "files"), exist_ok=True)
filename = str(uuid4())
with open(path.join(configGet("data", "locations"), "files", filename), "wb") as file:
file.write(filebytes)
return filename, path.join(configGet("data", "locations"), "files", filename)

View File

@ -1,5 +1,6 @@
fastapi[all] fastapi[all]
ujson~=5.7.0 ujson~=5.7.0
pymongo~=4.3.3
pydantic~=1.10.4 pydantic~=1.10.4
xmltodict~=0.13.0 xmltodict~=0.13.0
apscheduler~=3.9.1.post1 apscheduler~=3.9.1.post1

View File

@ -1,4 +1,4 @@
from os import makedirs, path from os import makedirs
from modules.app import app from modules.app import app
from modules.utils import configGet from modules.utils import configGet
from modules.extensions_loader import dynamic_import_from_src from modules.extensions_loader import dynamic_import_from_src
@ -7,15 +7,6 @@ from starlette.status import HTTP_200_OK
makedirs(configGet("data", "locations"), exist_ok=True) makedirs(configGet("data", "locations"), exist_ok=True)
for entry in [path.join(configGet("data", "locations"), "api_keys.json"), path.join(configGet("data", "locations"), "expired_keys.json")]:
mode = 'r' if path.exists(entry) else 'w'
with open(entry, mode) as f:
try:
f.write("[]")
except:
pass
@app.get("/check", response_class=Response, include_in_schema=False) @app.get("/check", response_class=Response, include_in_schema=False)
async def check(): async def check():
return Response(HTTP_200_OK) return Response(HTTP_200_OK)
@ -24,7 +15,6 @@ async def check():
async def favicon(): async def favicon():
return FileResponse("favicon.ico") return FileResponse("favicon.ico")
#================================================================================= #=================================================================================
dynamic_import_from_src("extensions", star_import = True) dynamic_import_from_src("extensions", star_import = True)
#================================================================================= #=================================================================================

27
sync_gen.py Normal file
View File

@ -0,0 +1,27 @@
from argparse import ArgumentParser
from json import dumps
from uuid import uuid4
from modules.database import col_apikeys
from modules.security import passEncode
# Args =====================================================================================================================================
parser = ArgumentParser(
prog = "Stardew Valley Sync API",
description = "Small subprogram made for API keys generation."
)
parser.add_argument("-u", "--username", help="Enter username without input prompt", action="store")
parser.add_argument("-j", "--json", help="Return output as a json. Username must be provided as an argument", action="store_true")
args = parser.parse_args()
#===========================================================================================================================================
username = input("Enter username: ") if args.username is None else args.username
new_key = str(uuid4())
col_apikeys.insert_one({"user": username, "hash": passEncode(new_key)})
if args.json is True and args.username is not None:
print(dumps({"apikey": new_key}))
else:
print(f"Generated API key for {username}: {new_key}", flush=True)

16
validation/apikeys.json Normal file
View File

@ -0,0 +1,16 @@
{
"$jsonSchema": {
"required": [
"user",
"hash"
],
"properties": {
"user": {
"bsonType": "string"
},
"hash": {
"bsonType": "binData"
}
}
}
}

24
validation/devices.json Normal file
View File

@ -0,0 +1,24 @@
{
"$jsonSchema": {
"required": [
"user",
"name",
"os",
"last_save"
],
"properties": {
"user": {
"bsonType": "string"
},
"name": {
"bsonType": "string"
},
"os": {
"bsonType": "string"
},
"last_save": {
"bsonType": ["int", "double"]
}
}
}
}

12
validation/expired.json Normal file
View File

@ -0,0 +1,12 @@
{
"$jsonSchema": {
"required": [
"hash"
],
"properties": {
"hash": {
"bsonType": "binData"
}
}
}
}

92
validation/saves.json Normal file
View File

@ -0,0 +1,92 @@
{
"$jsonSchema": {
"required": [
"id",
"user",
"device",
"date",
"data",
"data.farmer",
"data.money",
"data.played",
"data.save_time",
"data.year",
"data.season",
"data.day",
"files",
"files.save",
"files.save.name",
"files.save.uuid",
"files.save.path",
"files.saveinfo",
"files.saveinfo.name",
"files.saveinfo.uuid",
"files.saveinfo.path"
],
"properties": {
"id": {
"bsonType": "int"
},
"user": {
"bsonType": "string"
},
"device": {
"bsonType": "string"
},
"date": {
"bsonType": ["int", "double"]
},
"data": {
"bsonType": "object"
},
"data.farmer": {
"bsonType": "string"
},
"data.money": {
"bsonType": "int"
},
"data.played": {
"bsonType": "int"
},
"data.save_time": {
"bsonType": "int"
},
"data.year": {
"bsonType": "int"
},
"data.season": {
"bsonType": "int"
},
"data.day": {
"bsonType": "int"
},
"files": {
"bsonType": "object"
},
"files.save": {
"bsonType": "object"
},
"files.save.name": {
"bsonType": "string"
},
"files.save.uuid": {
"bsonType": "string"
},
"files.save.path": {
"bsonType": "string"
},
"files.saveinfo": {
"bsonType": "object"
},
"files.saveinfo.name": {
"bsonType": "string"
},
"files.saveinfo.uuid": {
"bsonType": "string"
},
"files.saveinfo.path": {
"bsonType": "string"
}
}
}
}