MongoDB migration
This commit is contained in:
parent
94f8839e53
commit
e9e9e3784a
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -1 +0,0 @@
|
|||||||
[]
|
|
@ -1 +0,0 @@
|
|||||||
[]
|
|
@ -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})
|
||||||
|
|
||||||
|
@ -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)
|
@ -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}))
|
||||||
|
|
||||||
|
if len(saves_entries) == 0:
|
||||||
|
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
for dir in listdir(save_path):
|
|
||||||
d = path.join(save_path, dir)
|
for entry in saves_entries:
|
||||||
if path.isdir(d):
|
out_entry = entry
|
||||||
output.append(jsonLoad(path.join(configGet("data", "locations"), "users", apikey, id, dir, "index.json"))) # type: ignore
|
del out_entry["_id"]
|
||||||
|
del out_entry["files"]
|
||||||
|
del out_entry["user"]
|
||||||
|
output.append(out_entry)
|
||||||
|
|
||||||
return UJSONResponse(output)
|
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,
|
||||||
|
"date": save_date,
|
||||||
|
"data": {
|
||||||
"farmer": save_info["Farmer"]["name"],
|
"farmer": save_info["Farmer"]["name"],
|
||||||
"year": int(save_info["Farmer"]["yearForSaveGame"]),
|
|
||||||
"season": int(save_info["Farmer"]["seasonForSaveGame"]),
|
|
||||||
"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"]),
|
||||||
"save_date": save_date,
|
"year": int(save_info["Farmer"]["yearForSaveGame"]),
|
||||||
"uploaded": now.isoformat()
|
"season": int(save_info["Farmer"]["seasonForSaveGame"]),
|
||||||
|
"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]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonSave(index, path.join(configGet("data", "locations"), "users", apikey, save_data["SaveGame"]["uniqueIDForThisGame"], save_date, "index.json")) # type: ignore
|
col_saves.insert_one(index)
|
||||||
|
|
||||||
|
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)
|
@ -1 +1,7 @@
|
|||||||
# from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Device(BaseModel):
|
||||||
|
user: str
|
||||||
|
name: str
|
||||||
|
os: str
|
||||||
|
last_save: int
|
@ -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
|
|
@ -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
34
modules/database.py
Normal 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
17
modules/security.py
Normal 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)
|
@ -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
|
||||||
|
|
||||||
@ -59,3 +61,18 @@ def configGet(key: str, *args: str) -> Any:
|
|||||||
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)
|
@ -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
|
12
sync_api.py
12
sync_api.py
@ -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
27
sync_gen.py
Normal 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
16
validation/apikeys.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$jsonSchema": {
|
||||||
|
"required": [
|
||||||
|
"user",
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"bsonType": "string"
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"bsonType": "binData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
validation/devices.json
Normal file
24
validation/devices.json
Normal 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
12
validation/expired.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$jsonSchema": {
|
||||||
|
"required": [
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"hash": {
|
||||||
|
"bsonType": "binData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
validation/saves.json
Normal file
92
validation/saves.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user