feat: hash objects for etag value

This commit is contained in:
Huskydog9988
2025-04-09 14:48:13 -04:00
parent e7566a6316
commit c4d8b24295
4 changed files with 59 additions and 15 deletions

View File

@ -12,15 +12,16 @@ export default defineEventHandler(async (h3) => {
throw createError({ statusCode: 404, statusMessage: "Object not found" }); throw createError({ statusCode: 404, statusMessage: "Object not found" });
// 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 etagValue = h3.headers.get("If-None-Match"); const etagRequestValue = h3.headers.get("If-None-Match");
if (etagValue !== null) { const etagActualValue = await objectHandler.fetchHash(id);
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);
return null; return null;
} }
// just return object id has etag since object should never change // TODO: fix undefined etagValue
setHeader(h3, "ETag", id); setHeader(h3, "ETag", etagActualValue ?? "");
setHeader(h3, "Content-Type", object.mime); setHeader(h3, "Content-Type", object.mime);
setHeader( setHeader(
h3, h3,

View File

@ -13,8 +13,9 @@ export default defineEventHandler(async (h3) => {
throw createError({ statusCode: 404, statusMessage: "Object not found" }); throw createError({ statusCode: 404, statusMessage: "Object not found" });
// 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 etagValue = h3.headers.get("If-None-Match"); const etagRequestValue = h3.headers.get("If-None-Match");
if (etagValue !== null) { const etagActualValue = await objectHandler.fetchHash(id);
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);
return null; return null;

View File

@ -7,15 +7,22 @@ import {
} from "./objectHandler"; } from "./objectHandler";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import { LRUCache } from "lru-cache";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { Readable, Stream } from "stream"; import { Readable, Stream } from "stream";
import { createHash } from "crypto";
export class FsObjectBackend extends ObjectBackend { export class FsObjectBackend extends ObjectBackend {
private baseObjectPath: string; private baseObjectPath: string;
private baseMetadataPath: string; private baseMetadataPath: string;
// TODO: should probably make this save into db or something if we agree to never
// overwrite an object
private cache = new LRUCache<string, string>({
max: 1000, // number of items
});
constructor() { constructor() {
super(); super();
const basePath = process.env.FS_BACKEND_PATH ?? "./.data/objects"; const basePath = process.env.FS_BACKEND_PATH ?? "./.data/objects";
@ -35,6 +42,9 @@ export class FsObjectBackend extends ObjectBackend {
const objectPath = path.join(this.baseObjectPath, sanitize(id)); const objectPath = path.join(this.baseObjectPath, sanitize(id));
if (!fs.existsSync(objectPath)) return false; if (!fs.existsSync(objectPath)) return false;
// remove item from cache
this.cache.delete(id);
if (source instanceof Readable) { if (source instanceof Readable) {
const outputStream = fs.createWriteStream(objectPath); const outputStream = fs.createWriteStream(objectPath);
source.pipe(outputStream, { end: true }); source.pipe(outputStream, { end: true });
@ -52,7 +62,8 @@ export class FsObjectBackend extends ObjectBackend {
async startWriteStream(id: ObjectReference) { async startWriteStream(id: ObjectReference) {
const objectPath = path.join(this.baseObjectPath, sanitize(id)); const objectPath = path.join(this.baseObjectPath, sanitize(id));
if (!fs.existsSync(objectPath)) return undefined; if (!fs.existsSync(objectPath)) return undefined;
// remove item from cache
this.cache.delete(id);
return fs.createWriteStream(objectPath); return fs.createWriteStream(objectPath);
} }
async create( async create(
@ -100,6 +111,8 @@ export class FsObjectBackend extends ObjectBackend {
const objectPath = path.join(this.baseObjectPath, sanitize(id)); const objectPath = path.join(this.baseObjectPath, sanitize(id));
if (!fs.existsSync(objectPath)) return true; if (!fs.existsSync(objectPath)) return true;
fs.rmSync(objectPath); fs.rmSync(objectPath);
// remove item from cache
this.cache.delete(id);
return true; return true;
} }
async fetchMetadata( async fetchMetadata(
@ -125,4 +138,26 @@ export class FsObjectBackend extends ObjectBackend {
fs.writeFileSync(metadataPath, JSON.stringify(metadata)); fs.writeFileSync(metadataPath, JSON.stringify(metadata));
return true; return true;
} }
async fetchHash(id: ObjectReference): Promise<string | undefined> {
const cacheResult = this.cache.get(id);
if (cacheResult !== undefined) return cacheResult;
const obj = await this.fetch(id);
if (obj === undefined) return;
// local variable to point to object
const cache = this.cache;
// hash object
const hash = createHash("md5");
hash.setEncoding("hex");
obj.on("end", function () {
hash.end();
cache.set(id, hash.read());
});
// read obj into hash
obj.pipe(hash);
return this.cache.get(id);
}
} }

View File

@ -49,20 +49,21 @@ 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>;
private async fetchMimeType(source: Source) { private async fetchMimeType(source: Source) {
if (source instanceof ReadableStream) { if (source instanceof ReadableStream) {
@ -86,7 +87,7 @@ 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)
@ -102,7 +103,7 @@ 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.createWithWriteStream(id, {
permissions, permissions,
@ -111,6 +112,12 @@ export abstract class ObjectBackend {
}); });
} }
/**
* Fetches object, but also checks if user has perms to access it
* @param id
* @param userId
* @returns
*/
async fetchWithPermissions(id: ObjectReference, userId?: string) { async fetchWithPermissions(id: ObjectReference, userId?: string) {
const metadata = await this.fetchMetadata(id); const metadata = await this.fetchMetadata(id);
if (!metadata) return; if (!metadata) return;
@ -147,7 +154,7 @@ 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.fetchMetadata(id);
if (!metadata) return false; if (!metadata) return false;