/** * Copyright (c) 2023 MERCENARIES.AI PTE. LTD. * All rights reserved. */ //--------------------------------------------------------- // Endpoints for uploading and grabbing files from storage //--------------------------------------------------------- import { type FastifyRequest, type FastifyReply } from 'fastify'; import { type CdnIntegration, type ICDNFidServeOpts } from '../CdnIntegration'; import type MercsServer from '../../../core/Server'; const fidClientExport = function () { return { description: 'Retrieve a workflow artifact', params: [{ name: 'fid', required: true, type: 'string' }] }; }; const uploadClientExport = function () { return { method: 'POST', description: 'Retrieve a workflow artifact', params: [{ name: 'fid', required: true, type: 'string' }] }; }; // Simple Upload Hander // TODO: Stream based version that can handle large files like video const createUploadHandler = function (integration: CdnIntegration, config: any) { return { schema: { headers: { type: 'object', properties: { 'Content-Type': { type: 'string', pattern: '.*multipart/form-data.*' // Ensures the request has this content-type. Right now this suffice, might need a custom validation function instead for more complex validation } }, required: ['content-type'] } }, handler: async function (request: FastifyRequest, reply: FastifyReply) { if (!request.user) { throw new Error('User not logged in'); } const parts = request.parts(); integration.info('upload', parts); const files = []; let storageType = 'temporary'; // Default storage type for await (const part of parts) { if (!(part as any).file) { // This is a non-file field const value = await (part as any).value; if (part.fieldname === 'storageType' && ['permanent', 'temporary'].includes(value)) { storageType = value; } } else { // This is a file const buffer = await (part as any).toBuffer(); const fileName = (part as any).filename; let res; if (storageType === 'permanent') { res = await integration.put(buffer, { fileName, userId: request.user.id, tags: ['upload'] }); } else { res = await integration.putTemp(buffer, { fileName, userId: request.user.id, tags: ['upload'] }); } files.push(res); } } return await reply.send(files); } }; }; const createFidHandler = function (integration: CdnIntegration, config: any) { return { schema: { params: { type: 'object', properties: { fid: { type: 'string' } }, required: ['fid'] }, querystring: { type: 'object', properties: { obj: { type: 'boolean' }, test: { type: 'boolean' } } } // TODO: Validate response }, handler: async function (request: FastifyRequest, reply: FastifyReply) { // const start = performance.now() //@ts-expect-error const fid = request.params.fid as string; if (fid == null) { return await reply.status(422).send({ error: 'Missing fid' }); } const cdn = (integration.app as MercsServer).cdn; //@ts-expect-error if (request.query.obj) { const fo = await cdn.find(fid); if (fo == null) { return await reply .status(404) .header('Cache-Control', 'no-cache, no-store, must-revalidate') .send({ error: 'File not found' }); } else { return await reply.status(200).send(fo); } } if ((request.query as any).test === 'true') { if (await cdn.checkFileExists(fid)) { return await reply.status(200).send({ exists: true }); } else { return await reply .status(410) .header('Cache-Control', 'no-cache, no-store, must-revalidate') .send({ exists: false }); } } const defaults = { download: false }; const opts = Object.assign({}, defaults, { ...(request.query as ICDNFidServeOpts) }); omnilog.log(opts); try { const servedFile = await cdn.serveFile(fid, opts, reply); return servedFile; } catch (ex: any) { integration.error(ex); const status = ex.response?.status ?? 500; const replied = reply.status(status).send({ error: `${status} : An error occurred` }); return await replied; } } }; }; export { createFidHandler, fidClientExport, createUploadHandler, uploadClientExport };