SyncAPI/extensions/saves.py

226 lines
9.3 KiB
Python
Raw Normal View History

2023-01-16 16:23:19 +02:00
from datetime import datetime
from io import BytesIO
2023-01-18 15:25:22 +02:00
from urllib.parse import quote_plus
2023-01-19 14:35:27 +02:00
from os import makedirs, path, remove
from typing import Dict, List, Tuple, Union
from uuid import uuid4
from zipfile import ZipFile, ZIP_DEFLATED
2023-01-16 16:23:19 +02:00
from xmltodict import parse
from models.saves import StardewSave
2023-01-18 15:25:22 +02:00
from modules.app import app, get_api_key, user_by_key
2023-01-19 14:35:27 +02:00
from modules.utils import configGet, saveFile
2023-01-18 15:25:22 +02:00
from modules.database import col_devices, col_saves
2023-01-16 16:23:19 +02:00
from fastapi import HTTPException, Depends, UploadFile
from fastapi.responses import UJSONResponse, FileResponse, Response
from fastapi.openapi.models import APIKey
2023-01-17 11:54:31 +02:00
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE
2023-01-16 16:23:19 +02:00
2023-01-19 14:35:27 +02:00
def zip_saves(save_filename: str, save_bytes: bytes, saveinfo_bytes: bytes) -> Tuple[str, str]:
save_uuid = str(uuid4())
makedirs(path.join(configGet("data", "locations"), "files", save_uuid))
#saveFile(save_bytes, filename=save_filename, dirname=zipname)
#saveFile(saveinfo_bytes, filename="SaveGameInfo", dirname=zipname)
with ZipFile(path.join(configGet("data", "locations"), "files", save_uuid, save_filename+".svsave"), 'w', ZIP_DEFLATED, compresslevel=configGet("compression")) as ziph:
ziph.writestr("SaveGameInfo", saveinfo_bytes)
ziph.writestr(save_filename, save_bytes)
return save_uuid, path.join(configGet("data", "locations"), "files", save_uuid, save_filename+".svsave")
2023-01-16 16:23:19 +02:00
def zipfiles(filenames, save_name: str) -> Response:
zip_filename = save_name+".svsave"
s = BytesIO()
zf = ZipFile(s, "w")
for fpath in filenames:
# Calculate path for file in zip
fdir, fname = path.split(fpath)
# Add file, at correct path
2023-01-18 15:25:22 +02:00
for entry in (list(col_saves.find({"files.save.uuid": fname})) + list(col_saves.find({"files.saveinfo.uuid": fname}))):
2023-01-19 14:35:27 +02:00
filename = entry["file"]["save"]["name"] if (entry["file"]["save"]["uuid"] == fname) else entry["file"]["saveinfo"]["name"]
2023-01-18 15:25:22 +02:00
zf.write(fpath, filename)
2023-01-16 16:23:19 +02:00
# Must close zip for all contents to be written
zf.close()
# Grab ZIP file from in-memory, make response with correct MIME-type
return Response(
s.getvalue(),
media_type="application/x-zip-compressed",
headers={
2023-01-18 15:25:22 +02:00
'Content-Disposition': f'attachment;filename={quote_plus(zip_filename)}'
2023-01-16 16:23:19 +02:00
}
)
@app.get("/saves", response_class=UJSONResponse, response_model=Dict[str, StardewSave], description="Get all available game saves")
2023-01-18 15:27:58 +02:00
async def saves_get(device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
2023-01-18 15:25:22 +02:00
2023-01-18 15:27:58 +02:00
user = user_by_key(apikey)
saves_entries = list(col_saves.find({"user": user})) if device is None else list(col_saves.find({"user": user_by_key(apikey), "user": user}))
2023-01-18 15:25:22 +02:00
if len(saves_entries) == 0:
2023-01-17 11:54:31 +02:00
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any saves.")
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
output = []
for entry in saves_entries:
out_entry = entry
del out_entry["_id"]
del out_entry["user"]
2023-01-19 14:35:27 +02:00
del out_entry["file"]
2023-01-18 15:25:22 +02:00
output.append(out_entry)
return UJSONResponse(output)
2023-01-16 16:23:19 +02:00
@app.get("/saves/{id}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
2023-01-18 15:25:22 +02:00
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:
2023-01-17 11:54:31 +02:00
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
output = []
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
for entry in saves_entries:
out_entry = entry
del out_entry["_id"]
2023-01-19 14:35:27 +02:00
del out_entry["file"]
2023-01-18 15:25:22 +02:00
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, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date}) if device is None else col_saves.find_one({"user": user_by_key(apikey), "id": id, "device": device, "date": save_date})
2023-01-18 15:25:22 +02:00
if saves_entry is not None:
del saves_entry["_id"]
2023-01-19 14:35:27 +02:00
del saves_entry["file"]
2023-01-18 15:25:22 +02:00
return UJSONResponse(saves_entry)
2023-01-17 11:54:31 +02:00
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
2023-01-18 15:25:22 +02:00
@app.delete("/saves/{id}", description="Get game saves by name")
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:
2023-01-19 14:35:27 +02:00
remove(entry["file"]["path"])
2023-01-18 15:25:22 +02:00
col_saves.delete_many({"user": user, "id": id})
2023-01-17 11:54:31 +02:00
return Response(status_code=HTTP_204_NO_CONTENT)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
2023-01-18 15:25:22 +02:00
@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:
2023-01-19 14:35:27 +02:00
remove(saves_entry["file"]["path"])
2023-01-18 15:25:22 +02:00
return Response(status_code=HTTP_204_NO_CONTENT)
2023-01-17 11:54:31 +02:00
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")
2023-01-19 14:35:27 +02:00
async def saves_download(id: int, save_date: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date}) if device is None else col_saves.find_one({"user": user_by_key(apikey), "id": id, "device": device, "date": save_date})
2023-01-18 15:25:22 +02:00
if saves_entry is not None: # type: ignore
2023-01-19 14:35:27 +02:00
with open(saves_entry["file"]["path"], "rb") as file:
response = Response(
file.read(),
media_type="application/x-zip-compressed",
headers={
'Content-Disposition': f'attachment;filename={quote_plus(saves_entry["file"]["name"])}.svsave'
}
)
return response
# return zipfiles([saves_entry["file"]["save"]["path"], saves_entry["file"]["saveinfo"]["path"]], save_name=f'{saves_entry["data"]["farmer"]}_{saves_entry["id"]}')
2023-01-16 16:23:19 +02:00
else:
2023-01-17 11:54:31 +02:00
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
2023-01-16 16:23:19 +02:00
@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)):
2023-01-18 15:25:22 +02:00
user = user_by_key(apikey)
2023-01-16 16:32:52 +02:00
error_return = HTTPException(HTTP_406_NOT_ACCEPTABLE, detail="You must provide two files: save file and SaveGameInfo for that save")
2023-01-16 16:23:19 +02:00
if len(files) != 2:
return error_return
2023-01-16 16:32:52 +02:00
if "SaveGameInfo" not in [file.filename for file in files]:
2023-01-16 16:23:19 +02:00
return error_return
2023-01-18 15:25:22 +02:00
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.")
2023-01-19 14:35:27 +02:00
save_info = save_data = save_info_file = save_data_filename = save_data_file = None
2023-01-16 16:23:19 +02:00
for file in files:
2023-01-16 16:32:52 +02:00
if file.filename == "SaveGameInfo":
2023-01-16 16:23:19 +02:00
save_info_file = await file.read()
save_info = parse(save_info_file.decode("utf-8"))
if "Farmer" not in save_info:
return error_return
else:
2023-01-18 15:25:22 +02:00
save_data_filename = file.filename
2023-01-16 16:23:19 +02:00
save_data_file = await file.read()
save_data = parse(save_data_file.decode("utf-8"))
if "SaveGame" not in save_data:
return error_return
2023-01-19 14:35:27 +02:00
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:
2023-01-16 16:23:19 +02:00
return error_return
2023-01-19 14:35:27 +02:00
zipped = zip_saves(save_data_filename, save_data_file, save_info_file)
2023-01-18 15:25:22 +02:00
save_date = int(datetime.utcnow().timestamp())
2023-01-16 16:23:19 +02:00
index = {
"id": int(save_data["SaveGame"]["uniqueIDForThisGame"]),
2023-01-18 15:25:22 +02:00
"user": user,
2023-01-16 16:23:19 +02:00
"device": device,
2023-01-18 15:25:22 +02:00
"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"])
},
2023-01-19 14:35:27 +02:00
"file": {
"name": save_data_filename,
"uuid": zipped[0],
"path": zipped[1]
2023-01-18 15:25:22 +02:00
}
2023-01-16 16:23:19 +02:00
}
2023-01-18 15:25:22 +02:00
col_saves.insert_one(index)
del save_info, save_data
del index["user"]
2023-01-19 14:35:27 +02:00
del index["file"]
2023-01-18 15:25:22 +02:00
del index["_id"]
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
col_devices.find_one_and_update({"user": user, "name": device}, {"$set": {"last_save": save_date}})
2023-01-16 16:23:19 +02:00
return UJSONResponse(index)