From 69bcee9aa9490ec22b6c1d8099898f77bad620d4 Mon Sep 17 00:00:00 2001 From: Pascal Kaufmann Date: Thu, 2 Jan 2025 10:19:54 +0100 Subject: [PATCH] Add fastify support for gridfs files --- examples/minimal/.env.defaults | 3 +- examples/minimal/boot.ts | 4 +- ...s-webhook.ts => gridfs-webhook-express.ts} | 0 .../files/gridfs/gridfs-webhook-fastify.ts | 118 ++++++++++++++++++ packages/plugins/src/presets/base-express.ts | 2 +- packages/plugins/src/presets/base-fastify.ts | 15 +++ 6 files changed, 138 insertions(+), 4 deletions(-) rename packages/plugins/src/files/gridfs/{gridfs-webhook.ts => gridfs-webhook-express.ts} (100%) create mode 100644 packages/plugins/src/files/gridfs/gridfs-webhook-fastify.ts create mode 100644 packages/plugins/src/presets/base-fastify.ts diff --git a/examples/minimal/.env.defaults b/examples/minimal/.env.defaults index e7664a0ddd..28a9893a29 100644 --- a/examples/minimal/.env.defaults +++ b/examples/minimal/.env.defaults @@ -10,5 +10,6 @@ UNCHAINED_CLOUD_ENDPOINT=https://engine.unchained.shop/graphql UNCHAINED_TOKEN_SECRET=random-token-that-is-not-secret-at-all UNCHAINED_COOKIE_SAMESITE=none UNCHAINED_COOKIE_INSECURE= +UNCHAINED_GRIDFS_PUT_UPLOAD_SECRET=secret REDIS_DB=0 -MONGOMS_VERSION=8.0.1 \ No newline at end of file +MONGOMS_VERSION=8.0.1 diff --git a/examples/minimal/boot.ts b/examples/minimal/boot.ts index a4a78d2105..52ddb70065 100644 --- a/examples/minimal/boot.ts +++ b/examples/minimal/boot.ts @@ -1,6 +1,6 @@ import { startPlatform, setAccessToken } from '@unchainedshop/platform'; import baseModules from '@unchainedshop/plugins/presets/base-modules.js'; -// import connectBasePluginsToExpress from '@unchainedshop/plugins/presets/base-express.js'; +import connectBasePluginsToFastify from '@unchainedshop/plugins/presets/base-fastify.js'; import { connect } from '@unchainedshop/api/lib/fastify/index.js'; import { createLogger } from '@unchainedshop/logger'; import seed from './seed.js'; @@ -47,7 +47,7 @@ const start = async () => { await setAccessToken(engine.unchainedAPI, 'admin', 'secret'); await connect(fastify, engine); - // await connectBasePluginsToExpress(app); + await connectBasePluginsToFastify(fastify); try { await fastify.listen({ port: process.env.PORT ? parseInt(process.env.PORT) : 3000 }); diff --git a/packages/plugins/src/files/gridfs/gridfs-webhook.ts b/packages/plugins/src/files/gridfs/gridfs-webhook-express.ts similarity index 100% rename from packages/plugins/src/files/gridfs/gridfs-webhook.ts rename to packages/plugins/src/files/gridfs/gridfs-webhook-express.ts diff --git a/packages/plugins/src/files/gridfs/gridfs-webhook-fastify.ts b/packages/plugins/src/files/gridfs/gridfs-webhook-fastify.ts new file mode 100644 index 0000000000..775d67a852 --- /dev/null +++ b/packages/plugins/src/files/gridfs/gridfs-webhook-fastify.ts @@ -0,0 +1,118 @@ +import { pipeline } from 'node:stream/promises'; +import { PassThrough } from 'node:stream'; +import { FastifyRequest, RouteHandlerMethod } from 'fastify'; +import { buildHashedFilename } from '@unchainedshop/file-upload'; +import sign from './sign.js'; +import { configureGridFSFileUploadModule } from './index.js'; +import { Context } from '@unchainedshop/api'; +import { createLogger } from '@unchainedshop/logger'; +import { getFileAdapter } from '@unchainedshop/core-files'; + +const logger = createLogger('unchained:plugins:gridfs'); + +export const gridfsHandler: RouteHandlerMethod = async ( + req: FastifyRequest & { + unchainedContext: Context & { + modules: { gridfsFileUploads: ReturnType }; + }; + }, + res, +) => { + try { + const { services, modules } = req.unchainedContext; + const { directoryName, fileName } = req.params as any; + + /* This is a file upload endpoint, and thus we need to allow CORS. + else we'd need proxies for all kinds of things for storefronts */ + res.header('Access-Control-Allow-Methods', 'GET, PUT'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + res.header('Access-Control-Allow-Origin', '*'); + + if (req.method === 'OPTIONS') { + res.status(200); + return res.send(); + } + + if (req.method === 'PUT') { + const { s: signature, e: expiryTimestamp } = req.query as Record; + const expiryDate = new Date(parseInt(expiryTimestamp as string, 10)); + const fileId = await buildHashedFilename(directoryName, fileName, expiryDate); + if ((await sign(directoryName, fileId, expiryDate.getTime())) === signature) { + const file = await modules.files.findFile({ fileId }); + if (file.expires === null) { + res.status(400); + return res.send('File already linked'); + } + // If the type is octet-stream, prefer mimetype lookup from the filename + // Else prefer the content-type header + const type = + req.headers['Content-Type'] === 'application/octet-stream' + ? file.type || (req.headers['Content-Type'] as string) + : (req.headers['Content-Type'] as string) || file.type; + + const writeStream = await modules.gridfsFileUploads.createWriteStream( + directoryName, + fileId, + fileName, + { 'content-type': type }, + ); + + await pipeline(req.raw, new PassThrough(), writeStream); + + const { length } = writeStream; + await services.files.linkFile({ fileId, size: length, type }); + + res.status(200); + return res.send(); + } + + res.status(403); + return res.send(); + } + + if (req.method === 'GET') { + const fileId = fileName; + const { s: signature, e: expiryTimestamp } = req.query as Record; + const file = await modules.gridfsFileUploads.getFileInfo(directoryName, fileId); + const fileDocument = await modules.files.findFile({ fileId }); + if (fileDocument?.meta?.isPrivate) { + const expiry = parseInt(expiryTimestamp as string, 10); + if (expiry <= Date.now()) { + res.status(403); + return res.send('Access restricted: Expired.'); + } + + const fileUploadAdapter = getFileAdapter(); + const signedUrl = await fileUploadAdapter.createDownloadURL(fileDocument, expiry); + + if (new URL(signedUrl, 'file://').searchParams.get('s') !== signature) { + res.status(403); + return res.send('Access restricted: Invalid signature.'); + } + } + if (file?.metadata?.['content-type']) { + res.header('Content-Type', file.metadata['content-type']); + } + if (file?.length) { + res.header('Content-Length', file?.length.toString()); + } + + const readStream = await modules.gridfsFileUploads.createReadStream(directoryName, fileId); + res.status(200); + return res.send(readStream); + } + + res.status(404); + return res.send(); + } catch (e) { + if (e.code === 'ENOENT') { + logger.warn(e); + res.status(404); + return res.send(JSON.stringify({ name: e.name, code: e.code, message: e.message })); + } else { + logger.warn(e); + res.status(504); + return res.send(JSON.stringify({ name: e.name, code: e.code, message: e.message })); + } + } +}; diff --git a/packages/plugins/src/presets/base-express.ts b/packages/plugins/src/presets/base-express.ts index e8d7e4eaed..57c48975e7 100644 --- a/packages/plugins/src/presets/base-express.ts +++ b/packages/plugins/src/presets/base-express.ts @@ -1,4 +1,4 @@ -import { gridfsHandler } from '../files/gridfs/gridfs-webhook.js'; +import { gridfsHandler } from '../files/gridfs/gridfs-webhook-express.js'; const { GRIDFS_PUT_SERVER_PATH = '/gridfs' } = process.env; diff --git a/packages/plugins/src/presets/base-fastify.ts b/packages/plugins/src/presets/base-fastify.ts new file mode 100644 index 0000000000..70bb538411 --- /dev/null +++ b/packages/plugins/src/presets/base-fastify.ts @@ -0,0 +1,15 @@ +import { FastifyInstance } from 'fastify'; +import { gridfsHandler } from '../files/gridfs/gridfs-webhook-fastify.js'; + +const { GRIDFS_PUT_SERVER_PATH = '/gridfs/:directoryName/:fileName' } = process.env; + +export default (fastify: FastifyInstance) => { + fastify.addContentTypeParser('*', function (req, payload, done) { + done(null); + }); + fastify.route({ + url: GRIDFS_PUT_SERVER_PATH, + method: ['GET', 'PUT', 'OPTIONS'], + handler: gridfsHandler, + }); +};