Files
drop/server/internal/saves/index.ts
DecDuck 63ac2b8ffc Depot API & v4 (#298)
* feat: nginx + torrential basics & services system

* fix: lint + i18n

* fix: update torrential to remove openssl

* feat: add torrential to Docker build

* feat: move to self hosted runner

* fix: move off self-hosted runner

* fix: update nginx.conf

* feat: torrential cache invalidation

* fix: update torrential for cache invalidation

* feat: integrity check task

* fix: lint

* feat: move to version ids

* fix: client fixes and client-side checks

* feat: new depot apis and version id fixes

* feat: update torrential

* feat: droplet bump and remove unsafe update functions

* fix: lint

* feat: v4 featureset: emulators, multi-launch commands

* fix: lint

* fix: mobile ui for game editor

* feat: launch options

* fix: lint

* fix: remove axios, use $fetch

* feat: metadata and task api improvements

* feat: task actions

* fix: slight styling issue

* feat: fix style and lints

* feat: totp backend routes

* feat: oidc groups

* fix: update drop-base

* feat: creation of passkeys & totp

* feat: totp signin

* feat: webauthn mfa/signin

* feat: launch selecting ui

* fix: manually running tasks

* feat: update add company game modal to use new SelectorGame

* feat: executor selector

* fix(docker): update rust to rust nightly for torrential build (#305)

* feat: new version ui

* feat: move package lookup to build time to allow for deno dev

* fix: lint

* feat: localisation cleanup

* feat: apply localisation cleanup

* feat: potential i18n refactor logic

* feat: remove args from commands

* fix: lint

* fix: lockfile

---------

Co-authored-by: Aden Lindsay <140392385+AdenMGB@users.noreply.github.com>
2026-01-13 15:32:39 +11:00

125 lines
3.4 KiB
TypeScript

import Stream from "node:stream";
import prisma from "../db/database";
import { applicationSettings } from "../config/application-configuration";
import objectHandler from "../objects";
import { randomUUID, createHash } from "node:crypto";
import type { IncomingMessage } from "node:http";
class SaveManager {
async deleteObjectFromSave(
gameId: string,
userId: string,
index: number,
objectId: string,
) {
await objectHandler.deleteWithPermission(objectId, userId);
}
async pushSave(
gameId: string,
userId: string,
index: number,
stream: IncomingMessage,
clientId: string | undefined = undefined,
) {
const save = await prisma.saveSlot.findUnique({
where: {
id: {
userId,
gameId,
index,
},
},
});
if (!save)
throw createError({ statusCode: 404, statusMessage: "Save not found" });
const newSaveObjectId = randomUUID();
const newSaveStream = await objectHandler.createWithStream(
newSaveObjectId,
{ saveSlot: JSON.stringify({ userId, gameId, index }) },
[],
);
if (!newSaveStream)
throw createError({
statusCode: 500,
statusMessage: "Failed to create writing stream to storage backend.",
});
let hash: string | undefined;
const hashPromise = Stream.promises.pipeline(
stream,
createHash("sha256").setEncoding("hex"),
async function (source) {
// @ts-expect-error Not sure how to get this to be typed
hash = (await source.toArray())[0];
},
);
const uploadStream = Stream.promises.pipeline(stream, newSaveStream);
await Promise.all([hashPromise, uploadStream]);
if (!hash) {
await objectHandler.deleteAsSystem(newSaveObjectId);
throw createError({
statusCode: 500,
statusMessage: "Hash failed to generate",
});
}
const newSaves = await prisma.saveSlot.updateManyAndReturn({
where: {
userId,
gameId,
index,
},
data: {
historyObjectIds: {
push: newSaveObjectId,
},
historyChecksums: {
push: hash,
},
...(clientId && { lastUsedClientId: clientId }),
},
});
const newSave = newSaves.at(0);
if (!newSave)
throw createError({ statusCode: 404, message: "Save not found" });
const historyLimit = await applicationSettings.get("saveSlotHistoryLimit");
if (newSave.historyObjectIds.length > historyLimit) {
// Delete previous
const safeFromIndex = newSave.historyObjectIds.length - historyLimit;
const toDelete = newSave.historyObjectIds.slice(0, safeFromIndex);
const toKeepObjects = newSave.historyObjectIds.slice(safeFromIndex);
const toKeepHashes = newSave.historyChecksums.slice(safeFromIndex);
// Delete objects first, so if we error out, we don't lose track of objects in backend
for (const objectId of toDelete) {
await this.deleteObjectFromSave(gameId, userId, index, objectId);
}
const { count } = await prisma.saveSlot.updateMany({
where: {
userId,
gameId,
index,
},
data: {
historyObjectIds: toKeepObjects,
historyChecksums: toKeepHashes,
},
});
if (count == 0) {
throw createError({ statusCode: 404, message: "Save not found" });
}
}
}
}
export const saveManager = new SaveManager();
export default saveManager;