feat: make internal objectbackend methods private

This commit is contained in:
Huskydog9988
2025-04-10 19:57:08 -04:00
parent 04c56fd985
commit dc89ff95d8
8 changed files with 67 additions and 32 deletions

View File

@ -3,9 +3,7 @@ import prisma from "~/server/internal/db/database";
import objectHandler from "~/server/internal/objects"; import objectHandler from "~/server/internal/objects";
export default defineEventHandler(async (h3) => { export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, [ const allowed = await aclManager.allowSystemACL(h3, ["game:image:delete"]);
"game:image:delete",
]);
if (!allowed) throw createError({ statusCode: 403 }); if (!allowed) throw createError({ statusCode: 403 });
const body = await readBody(h3); const body = await readBody(h3);
@ -37,7 +35,7 @@ export default defineEventHandler(async (h3) => {
throw createError({ statusCode: 400, statusMessage: "Image not found" }); throw createError({ statusCode: 400, statusMessage: "Image not found" });
game.mImageLibrary.splice(imageIndex, 1); game.mImageLibrary.splice(imageIndex, 1);
await objectHandler.delete(imageId); await objectHandler.deleteWithPermission(imageId);
if (game.mBannerId === imageId) { if (game.mBannerId === imageId) {
game.mBannerId = game.mImageLibrary[0]; game.mBannerId = game.mImageLibrary[0];

View File

@ -13,7 +13,10 @@ export default defineEventHandler(async (h3) => {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag
const etagRequestValue = h3.headers.get("If-None-Match"); const etagRequestValue = h3.headers.get("If-None-Match");
const etagActualValue = await objectHandler.fetchHash(id); const etagActualValue = await objectHandler.fetchHashWithWithPermissions(
id,
userId
);
if (etagRequestValue !== null && etagActualValue === etagRequestValue) { if (etagRequestValue !== null && etagActualValue === etagRequestValue) {
// would compare if etag is valid, but objects should never change // would compare if etag is valid, but objects should never change
setResponseStatus(h3, 304); setResponseStatus(h3, 304);

View File

@ -14,7 +14,10 @@ export default defineEventHandler(async (h3) => {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag
const etagRequestValue = h3.headers.get("If-None-Match"); const etagRequestValue = h3.headers.get("If-None-Match");
const etagActualValue = await objectHandler.fetchHash(id); const etagActualValue = await objectHandler.fetchHashWithWithPermissions(
id,
userId
);
if (etagRequestValue !== null && etagActualValue === etagRequestValue) { if (etagRequestValue !== null && etagActualValue === etagRequestValue) {
// would compare if etag is valid, but objects should never change // would compare if etag is valid, but objects should never change
setResponseStatus(h3, 304); setResponseStatus(h3, 304);

View File

@ -129,7 +129,7 @@ class NewsManager {
where: { id }, where: { id },
}); });
if (article.image) { if (article.image) {
return await objectHandler.delete(article.image); return await objectHandler.deleteWithPermission(article.image);
} }
return true; return true;
} }

View File

@ -1,5 +1,4 @@
import { import {
Object,
ObjectBackend, ObjectBackend,
ObjectMetadata, ObjectMetadata,
ObjectReference, ObjectReference,

View File

@ -1,3 +1,5 @@
import { FsObjectBackend } from "./fsBackend"; import { FsObjectBackend } from "./fsBackend";
export const objectHandler = new FsObjectBackend(); import { ObjectHandler } from "./objectHandler";
export default objectHandler
export const objectHandler = new ObjectHandler(new FsObjectBackend());
export default objectHandler;

View File

@ -49,21 +49,29 @@ export abstract class ObjectBackend {
abstract create( abstract create(
id: string, id: string,
source: Source, source: Source,
metadata: ObjectMetadata, metadata: ObjectMetadata
): Promise<ObjectReference | undefined>; ): Promise<ObjectReference | undefined>;
abstract createWithWriteStream( abstract createWithWriteStream(
id: string, id: string,
metadata: ObjectMetadata, metadata: ObjectMetadata
): Promise<Writable | undefined>; ): Promise<Writable | undefined>;
abstract delete(id: ObjectReference): Promise<boolean>; abstract delete(id: ObjectReference): Promise<boolean>;
abstract fetchMetadata( abstract fetchMetadata(
id: ObjectReference, id: ObjectReference
): Promise<ObjectMetadata | undefined>; ): Promise<ObjectMetadata | undefined>;
abstract writeMetadata( abstract writeMetadata(
id: ObjectReference, id: ObjectReference,
metadata: ObjectMetadata, metadata: ObjectMetadata
): Promise<boolean>; ): Promise<boolean>;
abstract fetchHash(id: ObjectReference): Promise<string | undefined>; abstract fetchHash(id: ObjectReference): Promise<string | undefined>;
}
export class ObjectHandler {
private backend: ObjectBackend;
constructor(backend: ObjectBackend) {
this.backend = backend;
}
private async fetchMimeType(source: Source) { private async fetchMimeType(source: Source) {
if (source instanceof ReadableStream) { if (source instanceof ReadableStream) {
@ -87,13 +95,13 @@ export abstract class ObjectBackend {
id: string, id: string,
sourceFetcher: () => Promise<Source>, sourceFetcher: () => Promise<Source>,
metadata: { [key: string]: string }, metadata: { [key: string]: string },
permissions: Array<string>, permissions: Array<string>
) { ) {
const { source, mime } = await this.fetchMimeType(await sourceFetcher()); const { source, mime } = await this.fetchMimeType(await sourceFetcher());
if (!mime) if (!mime)
throw new Error("Unable to calculate MIME type - is the source empty?"); throw new Error("Unable to calculate MIME type - is the source empty?");
await this.create(id, source, { await this.backend.create(id, source, {
permissions, permissions,
userMetadata: metadata, userMetadata: metadata,
mime, mime,
@ -103,9 +111,9 @@ export abstract class ObjectBackend {
async createWithStream( async createWithStream(
id: string, id: string,
metadata: { [key: string]: string }, metadata: { [key: string]: string },
permissions: Array<string>, permissions: Array<string>
) { ) {
return this.createWithWriteStream(id, { return this.backend.createWithWriteStream(id, {
permissions, permissions,
userMetadata: metadata, userMetadata: metadata,
mime: "application/octet-stream", mime: "application/octet-stream",
@ -119,7 +127,7 @@ export abstract class ObjectBackend {
* @returns * @returns
*/ */
async fetchWithPermissions(id: ObjectReference, userId?: string) { async fetchWithPermissions(id: ObjectReference, userId?: string) {
const metadata = await this.fetchMetadata(id); const metadata = await this.backend.fetchMetadata(id);
if (!metadata) return; if (!metadata) return;
// We only need one permission, so find instead of filter is faster // We only need one permission, so find instead of filter is faster
@ -137,7 +145,7 @@ export abstract class ObjectBackend {
// Because any permission can be read or up, we automatically know we can read this object // Because any permission can be read or up, we automatically know we can read this object
// So just straight return the object // So just straight return the object
const source = await this.fetch(id); const source = await this.backend.fetch(id);
if (!source) return undefined; if (!source) return undefined;
const object: Object = { const object: Object = {
data: source, data: source,
@ -146,6 +154,28 @@ export abstract class ObjectBackend {
return object; return object;
} }
async fetchHashWithWithPermissions(id: ObjectReference, userId?: string) {
const metadata = await this.backend.fetchMetadata(id);
if (!metadata) return;
// We only need one permission, so find instead of filter is faster
const myPermissions = metadata.permissions.find((e) => {
if (userId !== undefined && e.startsWith(userId)) return true;
if (userId !== undefined && e.startsWith("internal")) return true;
if (e.startsWith("anonymous")) return true;
return false;
});
if (!myPermissions) {
// We do not have access to this object
return;
}
// Because any permission can be read or up, we automatically know we can read this object
// So just straight return the object
return await this.backend.fetchHash(id);
}
// If we need to fetch a remote resource, it doesn't make sense // If we need to fetch a remote resource, it doesn't make sense
// to immediately fetch the object, *then* check permissions. // to immediately fetch the object, *then* check permissions.
// Instead the caller can pass a simple anonymous funciton, like // Instead the caller can pass a simple anonymous funciton, like
@ -154,9 +184,9 @@ export abstract class ObjectBackend {
async writeWithPermissions( async writeWithPermissions(
id: ObjectReference, id: ObjectReference,
sourceFetcher: () => Promise<Source>, sourceFetcher: () => Promise<Source>,
userId?: string, userId?: string
) { ) {
const metadata = await this.fetchMetadata(id); const metadata = await this.backend.fetchMetadata(id);
if (!metadata) return false; if (!metadata) return false;
const myPermissions = metadata.permissions const myPermissions = metadata.permissions
@ -178,13 +208,13 @@ export abstract class ObjectBackend {
if (!hasPermission) return false; if (!hasPermission) return false;
const source = await sourceFetcher(); const source = await sourceFetcher();
const result = await this.write(id, source); const result = await this.backend.write(id, source);
return result; return result;
} }
async deleteWithPermission(id: ObjectReference, userId?: string) { async deleteWithPermission(id: ObjectReference, userId?: string) {
const metadata = await this.fetchMetadata(id); const metadata = await this.backend.fetchMetadata(id);
if (!metadata) return false; if (!metadata) return false;
const myPermissions = metadata.permissions const myPermissions = metadata.permissions
@ -205,7 +235,7 @@ export abstract class ObjectBackend {
if (!hasPermission) return false; if (!hasPermission) return false;
const result = await this.delete(id); const result = await this.backend.delete(id);
return result; return result;
} }
} }

View File

@ -12,7 +12,7 @@ class SaveManager {
index: number, index: number,
objectId: string objectId: string
) { ) {
await objectHandler.delete(objectId); await objectHandler.deleteWithPermission(objectId, userId);
} }
async pushSave( async pushSave(
@ -62,7 +62,7 @@ class SaveManager {
await Promise.all([hashPromise, uploadStream]); await Promise.all([hashPromise, uploadStream]);
if (!hash) { if (!hash) {
await objectHandler.delete(newSaveObjectId); await objectHandler.deleteWithPermission(newSaveObjectId, userId);
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: "Hash failed to generate", statusMessage: "Hash failed to generate",