diff --git a/config.json b/config.json index 049814b..aaae890 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,11 @@ { + "database": { + "name": "stardew_sync", + "host": "127.0.0.1", + "port": 27017, + "user": null, + "password": null + }, "locations": { "data": "data" }, diff --git a/data/api_keys.json b/data/api_keys.json deleted file mode 100644 index 0637a08..0000000 --- a/data/api_keys.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/data/expired_keys.json b/data/expired_keys.json deleted file mode 100644 index 0637a08..0000000 --- a/data/expired_keys.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/extensions/apikey.py b/extensions/apikey.py index 99a6c37..2af6f0b 100644 --- a/extensions/apikey.py +++ b/extensions/apikey.py @@ -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}) diff --git a/extensions/devices.py b/extensions/devices.py index ff1409f..7b2f6a9 100644 --- a/extensions/devices.py +++ b/extensions/devices.py @@ -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 \ No newline at end of file + 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) \ No newline at end of file diff --git a/extensions/saves.py b/extensions/saves.py index 6f68ea6..9f9e852 100644 --- a/extensions/saves.py +++ b/extensions/saves.py @@ -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) \ No newline at end of file diff --git a/models/devices.py b/models/devices.py index 796bb0c..d643b0f 100644 --- a/models/devices.py +++ b/models/devices.py @@ -1 +1,7 @@ -# from pydantic import BaseModel \ No newline at end of file +from pydantic import BaseModel + +class Device(BaseModel): + user: str + name: str + os: str + last_save: int \ No newline at end of file diff --git a/models/saves.py b/models/saves.py index 9f24ab8..5a96e50 100644 --- a/models/saves.py +++ b/models/saves.py @@ -1,14 +1,16 @@ 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): id: int device: str - farmer: str - year: int - season: int - day: int - money: int - played: int - save_time: int - save_date: str - uploaded: str \ No newline at end of file + date: int + data: StardewSaveData \ No newline at end of file diff --git a/modules/app.py b/modules/app.py index 02184d0..59b4618 100644 --- a/modules/app.py +++ b/modules/app.py @@ -1,11 +1,14 @@ from os import path +from typing import Union from fastapi import FastAPI, Security, HTTPException from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN 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 starlette.status import HTTP_401_UNAUTHORIZED - 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") @@ -40,11 +43,8 @@ async def get_api_key( api_key_cookie: str = Security(api_key_cookie), ) -> str: - keys = get_all_api_keys() - expired = get_all_expired_keys() - 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): return api_key_query @@ -53,11 +53,15 @@ async def get_api_key( elif is_valid(api_key_cookie): return api_key_cookie 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")) else: 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) async def custom_swagger_ui_html(): return get_swagger_ui_html( diff --git a/modules/database.py b/modules/database.py new file mode 100644 index 0000000..ef1d1c0 --- /dev/null +++ b/modules/database.py @@ -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") \ No newline at end of file diff --git a/modules/security.py b/modules/security.py new file mode 100644 index 0000000..eff5d7b --- /dev/null +++ b/modules/security.py @@ -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) \ No newline at end of file diff --git a/modules/utils.py b/modules/utils.py index e50fcf9..700002a 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -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 traceback import print_exc @@ -58,4 +60,19 @@ def configGet(key: str, *args: str) -> Any: this_key = this_dict for dict_key in args: this_key = this_key[dict_key] - return this_key[key] \ No newline at end of file + 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) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ecfa816..90ea0a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ fastapi[all] ujson~=5.7.0 +pymongo~=4.3.3 pydantic~=1.10.4 xmltodict~=0.13.0 apscheduler~=3.9.1.post1 \ No newline at end of file diff --git a/sync_api.py b/sync_api.py index 4ddafee..57d22da 100644 --- a/sync_api.py +++ b/sync_api.py @@ -1,4 +1,4 @@ -from os import makedirs, path +from os import makedirs from modules.app import app from modules.utils import configGet 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) -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) async def check(): return Response(HTTP_200_OK) @@ -24,7 +15,6 @@ async def check(): async def favicon(): return FileResponse("favicon.ico") - #================================================================================= dynamic_import_from_src("extensions", star_import = True) #================================================================================= \ No newline at end of file diff --git a/sync_gen.py b/sync_gen.py new file mode 100644 index 0000000..c83bc00 --- /dev/null +++ b/sync_gen.py @@ -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) \ No newline at end of file diff --git a/validation/apikeys.json b/validation/apikeys.json new file mode 100644 index 0000000..1f67276 --- /dev/null +++ b/validation/apikeys.json @@ -0,0 +1,16 @@ +{ + "$jsonSchema": { + "required": [ + "user", + "hash" + ], + "properties": { + "user": { + "bsonType": "string" + }, + "hash": { + "bsonType": "binData" + } + } + } +} \ No newline at end of file diff --git a/validation/devices.json b/validation/devices.json new file mode 100644 index 0000000..e2d56d1 --- /dev/null +++ b/validation/devices.json @@ -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"] + } + } + } +} \ No newline at end of file diff --git a/validation/expired.json b/validation/expired.json new file mode 100644 index 0000000..6f72dd3 --- /dev/null +++ b/validation/expired.json @@ -0,0 +1,12 @@ +{ + "$jsonSchema": { + "required": [ + "hash" + ], + "properties": { + "hash": { + "bsonType": "binData" + } + } + } +} \ No newline at end of file diff --git a/validation/saves.json b/validation/saves.json new file mode 100644 index 0000000..8d48a6c --- /dev/null +++ b/validation/saves.json @@ -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" + } + } + } +} \ No newline at end of file