mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-14 16:51:15 +10:00
feat: hash objects for etag value
This commit is contained in:
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user