From 3bfcaabee3e97fa15dd6c4becb15a49dec8f2c9e Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Fri, 19 Jan 2024 17:30:25 -0500 Subject: [PATCH] experiments with saves --- backend/endpoints/saves.py | 28 +++++-- backend/models/assets.py | 3 +- backend/models/rom.py | 3 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- .../components/Dialog/Asset/DeleteAssets.vue | 9 +- frontend/src/services/api/save.ts | 14 ++++ frontend/src/views/Play/Base.vue | 84 ++++++++++++++++++- 8 files changed, 132 insertions(+), 15 deletions(-) diff --git a/backend/endpoints/saves.py b/backend/endpoints/saves.py index bd7de7cba..34d501a0f 100644 --- a/backend/endpoints/saves.py +++ b/backend/endpoints/saves.py @@ -1,11 +1,12 @@ from config.config_manager import config_manager as cm from decorators.auth import protected_route from endpoints.responses import MessageResponse -from endpoints.responses.assets import UploadedSavesResponse +from endpoints.responses.assets import UploadedSavesResponse, SaveSchema from fastapi import APIRouter, File, HTTPException, Request, UploadFile, status from handler import db_save_handler, fs_asset_handler, db_rom_handler from handler.scan_handler import scan_save from logger.logger import log +from config import LIBRARY_BASE_PATH router = APIRouter() @@ -55,9 +56,24 @@ def add_saves( # pass -# @protected_route(router.put, "/saves/{id}", ["assets.write"]) -# def update_save(request: Request, id: int) -> MessageResponse: -# pass +@protected_route(router.put, "/saves/{id}", ["assets.write"]) +async def update_save(request: Request, id: int) -> SaveSchema: + data = await request.form() + + db_save = db_save_handler.get_save(id) + if not db_save: + error = f"Save with ID {id} not found" + log.error(error) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error) + + if "file" in data: + file: UploadFile = data["file"] + fs_asset_handler._write_file( + file=file, path=f"{LIBRARY_BASE_PATH}/{db_save.file_path}" + ) + db_save_handler.update_save(db_save.id, {"file_size_bytes": file.size}) + + return db_save @protected_route(router.post, "/saves/delete", ["assets.write"]) @@ -84,7 +100,9 @@ async def delete_saves(request: Request) -> MessageResponse: log.info(f"Deleting {save.file_name} from filesystem") try: - fs_asset_handler.remove_file(file_name=save.file_name, file_path=save.file_path) + fs_asset_handler.remove_file( + file_name=save.file_name, file_path=save.file_path + ) except FileNotFoundError: error = f"Save file {save.file_name} not found for platform {save.rom.platform_slug}" log.error(error) diff --git a/backend/models/assets.py b/backend/models/assets.py index d3820a468..156206b1b 100644 --- a/backend/models/assets.py +++ b/backend/models/assets.py @@ -1,3 +1,4 @@ +import secrets from functools import cached_property from models.base import BaseModel @@ -33,7 +34,7 @@ class BaseAsset(BaseModel): @cached_property def download_path(self) -> str: - return f"/api/raw/{self.full_path}?timestamp={self.updated_at}" + return f"/api/raw/{self.full_path}?s={secrets.token_hex(8)}" class Save(BaseAsset): diff --git a/backend/models/rom.py b/backend/models/rom.py index e999c7c82..34f22d684 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -1,4 +1,5 @@ import re +import secrets from functools import cached_property from config import ( @@ -85,7 +86,7 @@ class Rom(BaseModel): @cached_property def download_path(self) -> str: - return f"/api/raw/{self.full_path}" + return f"/api/raw/{self.full_path}?s={secrets.token_hex(8)}" @cached_property def has_cover(self) -> bool: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 675f816d7..c5bd05647 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,7 @@ "axios": "^1.6.0", "core-js": "^3.8.3", "cronstrue": "^2.31.0", - "emulatorjs": "github:GAntoine/emulatorjs", + "emulatorjs": "github:GAntoine/emulatorjs#main", "file-saver": "^2.0.5", "js-cookie": "^3.0.5", "jszip": "^3.10.1", @@ -3191,7 +3191,7 @@ }, "node_modules/emulatorjs": { "version": "4.0.6", - "resolved": "git+ssh://git@github.com/GAntoine/emulatorjs.git#8015836f1dbed7d75b7ab77bf94669e58a77bee0", + "resolved": "git+ssh://git@github.com/GAntoine/emulatorjs.git#8dfbfb0f32e94c77d49492fa34d27723a29d175e", "license": "GPL-3.0", "dependencies": { "http-server": "^14.1.1" diff --git a/frontend/package.json b/frontend/package.json index e3185a5ff..72732d422 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,7 @@ "axios": "^1.6.0", "core-js": "^3.8.3", "cronstrue": "^2.31.0", - "emulatorjs": "github:GAntoine/emulatorjs", + "emulatorjs": "github:GAntoine/emulatorjs#main", "file-saver": "^2.0.5", "js-cookie": "^3.0.5", "jszip": "^3.10.1", diff --git a/frontend/src/components/Dialog/Asset/DeleteAssets.vue b/frontend/src/components/Dialog/Asset/DeleteAssets.vue index 90f4828c9..8ce5ed30d 100644 --- a/frontend/src/components/Dialog/Asset/DeleteAssets.vue +++ b/frontend/src/components/Dialog/Asset/DeleteAssets.vue @@ -47,9 +47,12 @@ async function deleteAssets() { }); result - .then(({ data }) => { - if (romRef.value) { - romRef.value[assetType.value] = data; + .then(() => { + if (romRef.value?.[assetType.value]) { + const deletedAssetIds = assets.value.map((asset) => asset.id); + romRef.value[assetType.value] = romRef.value[assetType.value].filter( + (asset) => !deletedAssetIds.includes(asset.id) + ); romsStore.update(romRef.value); emitter?.emit("romUpdated", romRef.value); } diff --git a/frontend/src/services/api/save.ts b/frontend/src/services/api/save.ts index 1006f66cf..373ecda05 100644 --- a/frontend/src/services/api/save.ts +++ b/frontend/src/services/api/save.ts @@ -15,6 +15,19 @@ async function uploadSaves({ rom, saves }: { rom: Rom; saves: File[] }) { }); } +async function updateSave({ + save, + file, +}: { + save: SaveSchema; + file: File; +}): Promise<{ data: SaveSchema }> { + var formData = new FormData(); + formData.append("file", file); + + return api.put(`/saves/${save.id}`, formData); +} + async function deleteSaves({ saves, deleteFromFs, @@ -30,5 +43,6 @@ async function deleteSaves({ export default { deleteSaves, + updateSave, uploadSaves, }; diff --git a/frontend/src/views/Play/Base.vue b/frontend/src/views/Play/Base.vue index e08c5af26..bbfce178a 100644 --- a/frontend/src/views/Play/Base.vue +++ b/frontend/src/views/Play/Base.vue @@ -3,6 +3,7 @@ import { ref, onMounted } from "vue"; import { useRoute } from "vue-router"; import romApi from "@/services/api/rom"; import stateApi from "@/services/api/state"; +import saveApi, { saveApi as api } from "@/services/api/save"; import type { Rom } from "@/stores/roms"; import type { SaveSchema, StateSchema } from "@/__generated__"; import { formatBytes } from "@/utils"; @@ -39,6 +40,8 @@ declare global { EJS_onGameStart: () => void; EJS_onSaveState: (args: { screenshot: File; state: File }) => void; EJS_onLoadState: () => void; + EJS_onSaveSave: (args: { screenshot: File; save: File }) => void; + EJS_onLoadSave: () => void; } } @@ -51,7 +54,7 @@ window.EJS_pathtodata = "/assets/emulatorjs/"; window.EJS_color = "#A453FF"; window.EJS_alignStartButton = "center"; window.EJS_startOnLoaded = true; -window.EJS_fullscreenOnLoaded = true; +window.EJS_fullscreenOnLoaded = false; window.EJS_defaultOptions = { "save-state-location": "browser", }; @@ -72,6 +75,22 @@ function buildStateName(rom: Rom): string { return stateName; } +function buildSaveName(rom: Rom): string { + const saves = rom.saves.map((s) => s.file_name); + const romName = rom.file_name.replace(EXTENSION_REGEX, "").trim(); + let saveName = `${romName}.srm`; + if (!saves.includes(saveName)) return saveName; + + let i = 2; + saveName = `${romName} (${i}).srm`; + while (saves.includes(saveName)) { + i++; + saveName = `${romName} (${i}).srm`; + } + + return saveName; +} + window.EJS_onSaveState = function ({ state, }: { @@ -115,7 +134,69 @@ window.EJS_onSaveState = function ({ } }; +async function getSave(): Promise { + if (saveRef.value) { + const { data } = await api.get(saveRef.value.download_path.replace("/api", "")); + var enc = new TextEncoder(); + return enc.encode(data); + } else { + const file = await window.EJS_emulator.selectFile(); + return new Uint8Array(await file.arrayBuffer()); + } +}; + +window.EJS_onLoadSave = async function () { + const sav = await getSave(); + const FS = window.EJS_emulator.Module.FS; + const path = window.EJS_emulator.gameManager.getSaveFilePath(); + const paths = path.split("/"); + let cp = ""; + for (let i=0; i { + saveRef.value = data; + }); + } else if (rom.value) { + saveApi + .uploadSaves({ + rom: rom.value, + saves: [ + new File([save], buildSaveName(rom.value), { + type: "application/octet-stream", + }), + ], + }) + .then(({ data }) => { + if (rom.value) rom.value.saves = data.saves; + saveRef.value = data.saves.pop() ?? null; + }); + } +}; + window.EJS_onGameStart = () => { + if (saveRef.value) window.EJS_onLoadSave(); gameRunning.value = true; }; @@ -147,7 +228,6 @@ function onPlay() {