MongoDB migration

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

View File

@@ -1,9 +1,8 @@
from os import path
from uuid import uuid4
from shutil import move
from models.apikey import APIKeyUpdated
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.responses import UJSONResponse
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")
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())
keys_valid.remove(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
col_apikeys.find_one_and_replace({"hash": passEncode(apikey)}, {"hash": passEncode(new_key)})
col_expired.insert_one({"hash": passEncode(apikey)})
return UJSONResponse({"apikey": new_key})

View File

@@ -1,21 +1,72 @@
# from modules.app import app, get_api_key
# from modules.utils import configGet, jsonLoad
# from fastapi import HTTPException, Depends
# from fastapi.responses import UJSONResponse, FileResponse
# from fastapi.openapi.models import APIKey
from modules.app import app, get_api_key, user_by_key
from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends
from fastapi.responses import UJSONResponse, Response
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")
# async def devices_get(apikey: APIKey = Depends(get_api_key)):
# pass
@app.get("/devices", response_class=UJSONResponse, description="Get all devices")
async def devices_get(apikey: APIKey = Depends(get_api_key)):
# @app.get("/devices/{name}", response_class=UJSONResponse, description="Get game saves from device by name")
# async def devices_get_by_name(name: str, apikey: APIKey = Depends(get_api_key)):
# pass
devices = list(col_devices.find({"user": user_by_key(apikey)}))
# @app.post("/devices", response_class=UJSONResponse, description="Create new device")
# async def devices_post(name: str, apikey: APIKey = Depends(get_api_key)):
# pass
if len(devices) == 0:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any devices.")
# @app.put("/devices/{name}", response_class=UJSONResponse, description="Update name of the existing device")
# async def devices_put(name: str, apikey: APIKey = Depends(get_api_key)):
# pass
output = []
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 io import BytesIO
from os import listdir, makedirs, path, rmdir, sep
from typing import Dict, List
from urllib.parse import quote_plus
from os import path, remove
from typing import Dict, List, Union
from zipfile import ZipFile
from xmltodict import parse
from models.saves import StardewSave
from modules.app import app, get_api_key
from modules.utils import configGet, jsonLoad, jsonSave
from modules.app import app, get_api_key, user_by_key
from modules.utils import saveFile
from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends, UploadFile
from fastapi.responses import UJSONResponse, FileResponse, Response
from fastapi.openapi.models import APIKey
@@ -26,7 +28,9 @@ def zipfiles(filenames, save_name: str) -> Response:
fdir, fname = path.split(fpath)
# 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
zf.close()
@@ -36,78 +40,92 @@ def zipfiles(filenames, save_name: str) -> Response:
s.getvalue(),
media_type="application/x-zip-compressed",
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")
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):
output = {}
for id in listdir(save_path):
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:
saves_entries = list(col_saves.find({"user": user_by_key(apikey)}))
if len(saves_entries) == 0:
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")
async def saves_get_by_id(id: str, apikey: APIKey = Depends(get_api_key)):
save_path = path.join(configGet("data", "locations"), "users", apikey, id) # type: ignore
if path.exists(save_path):
output = []
for dir in listdir(save_path):
d = path.join(save_path, dir)
if path.isdir(d):
output.append(jsonLoad(path.join(configGet("data", "locations"), "users", apikey, id, dir, "index.json"))) # type: ignore
return UJSONResponse(output)
async def saves_get_by_id(id: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
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 = []
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:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.delete("/saves/{id}", description="Get game saves by name")
async def saves_delete_by_id(id: str, apikey: APIKey = Depends(get_api_key)):
save_path = path.join(configGet("data", "locations"), "users", apikey, id) # type: ignore
if path.exists(save_path):
rmdir(save_path)
async def saves_delete_by_id(id: int, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
if col_saves.count_documents({"user": user, "id": id}) > 0:
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)
else:
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")
async def saves_delete_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):
rmdir(save_path)
@app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, description="Get game saves by name")
async def saves_delete_by_both_ids(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one_and_delete({"id": id, "date": save_date})
if saves_entry is not None:
remove(saves_entry["files"]["save"]["path"])
remove(saves_entry["files"]["saveinfo"]["path"])
return Response(status_code=HTTP_204_NO_CONTENT)
else:
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")
async def saves_download(id: str, save_date: str, apikey: APIKey = Depends(get_api_key)):
if path.exists(path.join(configGet("data", "locations"), "users", apikey, id, save_date)): # type: ignore
save_path = path.join(configGet("data", "locations"), "users", apikey, id, save_date) # 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}")
async def saves_download(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: # type: ignore
return zipfiles([saves_entry["files"]["save"]["path"], saves_entry["files"]["saveinfo"]["path"]], save_name=f'{saves_entry["data"]["farmer"]}_{saves_entry["id"]}')
else:
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")
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")
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]:
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:
if file.filename == "SaveGameInfo":
save_info_file = await file.read()
save_info_file_id = saveFile(save_info_file)
save_info = parse(save_info_file.decode("utf-8"))
if "Farmer" not in save_info:
return error_return
else:
save_data_filename = file.filename
save_data_file = await file.read()
save_data_file_id = saveFile(save_data_file)
save_data = parse(save_data_file.decode("utf-8"))
if "SaveGame" not in save_data:
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
now = datetime.now()
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)
save_date = int(datetime.utcnow().timestamp())
index = {
"id": int(save_data["SaveGame"]["uniqueIDForThisGame"]),
"user": user,
"device": device,
"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"]),
"played": int(save_info["Farmer"]["millisecondsPlayed"]),
"save_time": int(save_info["Farmer"]["saveTime"]),
"save_date": save_date,
"uploaded": now.isoformat()
"date": save_date,
"data": {
"farmer": save_info["Farmer"]["name"],
"money": int(save_info["Farmer"]["money"]),
"played": int(save_info["Farmer"]["millisecondsPlayed"]),
"save_time": int(save_info["Farmer"]["saveTime"]),
"year": int(save_info["Farmer"]["yearForSaveGame"]),
"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]
}
}
}
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)