experiments with saves

This commit is contained in:
Georges-Antoine Assi
2024-01-19 17:30:25 -05:00
parent 2164a937ee
commit 3bfcaabee3
8 changed files with 132 additions and 15 deletions

View File

@@ -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)

View File

@@ -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):

View File

@@ -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:

View File

@@ -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"

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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,
};

View File

@@ -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<Uint8Array> {
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<paths.length-1; i++) {
if (paths[i] === "") continue;
cp += "/"+paths[i];
if (!FS.analyzePath(cp).exists) FS.mkdir(cp);
}
if (FS.analyzePath(path).exists) FS.unlink(path);
FS.writeFile(path, sav);
window.EJS_emulator.gameManager.loadSaveFiles();
};
window.EJS_onSaveSave = function ({
save,
}: {
screenshot: File;
save: File;
}) {
if (saveRef.value) {
saveApi
.updateSave({
save: saveRef.value,
file: new File([save], saveRef.value.file_name, {
type: "application/octet-stream",
}),
})
.then(({ data }) => {
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() {
<v-col v-if="rom && !gameRunning" class="v-col-3">
<v-select
clearable
disabled
label="Save"
v-model="saveRef"
:items="