2023-01-16 16:23:19 +02:00
|
|
|
from datetime import datetime
|
2023-01-18 15:25:22 +02:00
|
|
|
from urllib.parse import quote_plus
|
2023-01-19 14:47:18 +02:00
|
|
|
from os import remove
|
2023-01-21 19:02:36 +02:00
|
|
|
from typing import Dict, List, Literal, Union
|
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
|
|
|
|
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-19 15:11:49 +02:00
|
|
|
from starlette.status import HTTP_204_NO_CONTENT, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE
|
2023-01-16 16:23:19 +02:00
|
|
|
|
2023-01-19 15:11:49 +02:00
|
|
|
from modules.utils import configGet, zip_saves
|
2023-01-16 16:23:19 +02:00
|
|
|
|
|
|
|
|
2023-01-22 12:31:00 +02:00
|
|
|
@app.get("/saves", response_class=UJSONResponse, response_model=Union[List[Dict[str, StardewSave]], List[int]], description="Get all available game saves")
|
|
|
|
async def saves_get(device: Union[str, None] = None, version: Union[str, None] = None, only_ids: bool = False, 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)
|
2023-01-19 14:58:18 +02:00
|
|
|
|
|
|
|
query = {"user": user}
|
|
|
|
|
|
|
|
if device is not None:
|
|
|
|
query["device"] = device
|
|
|
|
|
|
|
|
if version is not None:
|
|
|
|
query["data.game_version"] = version
|
|
|
|
|
|
|
|
saves_entries = list(col_saves.find(query))
|
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 = []
|
|
|
|
|
2023-01-22 12:31:00 +02:00
|
|
|
if only_ids is True:
|
|
|
|
for entry in saves_entries:
|
|
|
|
if entry["id"] not in output:
|
|
|
|
output.append(entry["id"])
|
|
|
|
else:
|
|
|
|
for entry in saves_entries:
|
|
|
|
out_entry = entry
|
|
|
|
del out_entry["_id"]
|
|
|
|
del out_entry["user"]
|
|
|
|
del out_entry["file"]
|
|
|
|
output.append(out_entry)
|
2023-01-18 15:25:22 +02:00
|
|
|
|
|
|
|
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-19 14:58:18 +02:00
|
|
|
async def saves_get_by_id(id: int, device: Union[str, None] = None, version: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
|
|
|
|
|
|
|
|
query = {"id": id}
|
|
|
|
|
|
|
|
if device is not None:
|
|
|
|
query["device"] = device
|
2023-01-18 15:25:22 +02:00
|
|
|
|
2023-01-19 14:58:18 +02:00
|
|
|
if version is not None:
|
|
|
|
query["data.game_version"] = version
|
|
|
|
|
|
|
|
saves_entries = list(col_saves.find(query))
|
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 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)
|
|
|
|
|
|
|
|
|
2023-01-21 19:02:36 +02:00
|
|
|
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by id and upload date")
|
|
|
|
async def saves_get_by_both_ids(id: int, save_date: Union[int, Literal["latest"]], device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
|
2023-01-19 14:58:18 +02:00
|
|
|
|
|
|
|
query = {"user": user_by_key(apikey), "id": id, "date": save_date}
|
|
|
|
|
|
|
|
if device is not None:
|
|
|
|
query["device"] = device
|
|
|
|
|
|
|
|
saves_entry = col_saves.find_one(query)
|
|
|
|
|
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-21 19:02:36 +02:00
|
|
|
@app.delete("/saves/{id}", description="Delete game saves by id")
|
2023-01-18 15:25:22 +02:00
|
|
|
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-21 19:02:36 +02:00
|
|
|
@app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, description="Delete game saves by id and upload date")
|
2023-01-18 15:25:22 +02:00
|
|
|
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.")
|
|
|
|
|
|
|
|
|
2023-01-21 19:02:36 +02:00
|
|
|
@app.get("/saves/{id}/{save_date}/download", response_class=FileResponse, description="Get game save as .svsave file by its id and upload 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
|
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
|
|
|
|
2023-01-19 15:13:14 +02:00
|
|
|
if (configGet("saves", "limits") != -1) and (col_saves.count_documents({"user": user}) >= configGet("saves", "limits")):
|
2023-01-19 15:11:49 +02:00
|
|
|
return UJSONResponse(
|
|
|
|
{
|
|
|
|
"detail": f'Too many save files. This instance allows to store only {configGet("saves", "limits")} saves per user',
|
|
|
|
"limit": configGet("saves", "limits")
|
|
|
|
},
|
|
|
|
status_code=HTTP_403_FORBIDDEN
|
|
|
|
)
|
|
|
|
|
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"]),
|
2023-01-19 14:47:18 +02:00
|
|
|
"day": int(save_info["Farmer"]["dayOfMonthForSaveGame"]),
|
|
|
|
"game_version": save_info["Farmer"]["gameVersion"]
|
2023-01-18 15:25:22 +02:00
|
|
|
},
|
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)
|