diff --git a/prisma/migrations/20250508153613_add_screenshots/migration.sql b/prisma/migrations/20250508153613_add_screenshots/migration.sql new file mode 100644 index 0000000..08869af --- /dev/null +++ b/prisma/migrations/20250508153613_add_screenshots/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "Screenshot" ( + "id" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "objectId" TEXT NOT NULL, + "private" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMPTZ(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Screenshot_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Screenshot_gameId_userId_idx" ON "Screenshot"("gameId", "userId"); + +-- AddForeignKey +ALTER TABLE "Screenshot" ADD CONSTRAINT "Screenshot_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Screenshot" ADD CONSTRAINT "Screenshot_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma index 3264c78..c321c56 100644 --- a/prisma/models/content.prisma +++ b/prisma/models/content.prisma @@ -35,6 +35,7 @@ model Game { collections CollectionEntry[] saves SaveSlot[] + screenshots Screenshot[] @@unique([metadataSource, metadataId], name: "metadataKey") } @@ -85,6 +86,22 @@ model SaveSlot { @@id([gameId, userId, index], name: "id") } +model Screenshot { + id String @id @default(uuid()) + + gameId String + game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + objectId String + private Boolean @default(true) + + createdAt DateTime @default(now()) @db.Timestamptz(0) + + @@index([gameId, userId]) +} + model Developer { id String @id @default(uuid()) diff --git a/prisma/models/user.prisma b/prisma/models/user.prisma index 823bdd0..9a578a1 100644 --- a/prisma/models/user.prisma +++ b/prisma/models/user.prisma @@ -17,7 +17,8 @@ model User { tokens APIToken[] sessions Session[] - saves SaveSlot[] + saves SaveSlot[] + screenshots Screenshot[] } model Notification { diff --git a/server/internal/saves/index.ts b/server/internal/saves/index.ts index 3d41035..69e4d11 100644 --- a/server/internal/saves/index.ts +++ b/server/internal/saves/index.ts @@ -1,9 +1,9 @@ -import Stream from "stream"; +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 "http"; +import type { IncomingMessage } from "node:http"; class SaveManager { async deleteObjectFromSave( diff --git a/server/internal/screenshots/index.ts b/server/internal/screenshots/index.ts new file mode 100644 index 0000000..e8b4911 --- /dev/null +++ b/server/internal/screenshots/index.ts @@ -0,0 +1,58 @@ +import { randomUUID } from "node:crypto"; +import type { IncomingMessage } from "node:http"; +import objectHandler from "../objects"; +import stream from "node:stream/promises"; +import prisma from "../db/database"; + +class ScreenshotManager { + async get(id: string) { + return await prisma.screenshot.findUnique({ + where: { + id, + }, + }); + } + + async getAllByGame(gameId: string, userId: string) { + const results = await prisma.screenshot.findMany({ + where: { + gameId, + userId, + }, + }); + return results; + } + + async delete(id: string) { + await prisma.screenshot.delete({ + where: { + id, + }, + }); + } + + async upload(gameId: string, userId: string, inputStream: IncomingMessage) { + const objectId = randomUUID(); + const saveStream = await objectHandler.createWithStream(objectId, {}, []); + if (!saveStream) + throw createError({ + statusCode: 500, + statusMessage: "Failed to create writing stream to storage backend.", + }); + + // pipe into object store + await stream.pipeline(inputStream, saveStream); + + // TODO: set createAt to the time screenshot was taken + await prisma.screenshot.create({ + data: { + gameId, + userId, + objectId, + }, + }); + } +} + +export const screenshotManager = new ScreenshotManager(); +export default screenshotManager;