UNPKG

@foal/storage

Version:

Storage components for FoalTS

173 lines (172 loc) 6.82 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ParseAndValidateFiles = ParseAndValidateFiles; // std const path_1 = require("path"); const stream_1 = require("stream"); const util_1 = require("util"); // 3p const core_1 = require("@foal/core"); const createBusboy = require("busboy"); // FoalTS const disk_service_1 = require("./disk.service"); function ParseAndValidateFiles(filesSchema, fieldsSchema = { type: 'object', properties: {} }, options = {}) { async function hook(ctx, services) { const disk = services.get(disk_service_1.Disk); const fileSizeLimit = core_1.Config.get('settings.multipartRequests.fileSizeLimit', 'number'); const fileNumberLimit = core_1.Config.get('settings.multipartRequests.fileNumberLimit', 'number'); let busboy; try { busboy = createBusboy({ headers: ctx.request.headers, limits: { fileSize: fileSizeLimit, files: fileNumberLimit } }); } catch (error) { return new core_1.HttpResponseBadRequest({ headers: { error: 'INVALID_MULTIPART_FORM_DATA_REQUEST', message: error.message } }); } let sizeLimitReached = false; let numberLimitReached = false; const uploads = []; busboy.on('field', (name, value) => ctx.request.body[name] = value); busboy.on('file', (name, stream, info) => { const { filename, encoding, mimeType } = info; if (!(name in filesSchema)) { stream.resume(); return; } stream.on('limit', () => sizeLimitReached = name); uploads.push(new Promise(async (resolve) => { try { const { saveTo } = filesSchema[name]; const extension = (0, path_1.extname)(filename || '').replace('.', ''); const file = new core_1.File({ buffer: saveTo === undefined ? await (0, core_1.streamToBuffer)(stream) : undefined, encoding, filename, mimeType, path: saveTo !== undefined ? (await disk.write(saveTo, stream, { extension })).path : undefined, }); ctx.files.push(name, file); resolve({}); } catch (error) { stream.resume(); resolve({ error }); } })); }); busboy.on('filesLimit', () => numberLimitReached = true); ctx.request.pipe(busboy); await (0, util_1.promisify)(stream_1.finished)(busboy); async function deleteUploadedFiles() { await Promise.allSettled(ctx.files.getAll().map(({ path }) => path && disk.delete(path))); } // Wait for all saves to finish. // When busboy "finish" event is emitted, it means all busboy streams have ended. // It does not mean that other Disk streams/promises have ended/been resolved. const errors = (await Promise.all(uploads)) .filter(({ error }) => !!error) .map(({ error }) => error); // We can only rely upon resolved promises to detect errors in the "finish" handlers. // Otherwise, we would attach a "catch" handler _after_ the promise may have been rejected. // The code below is an example of this problem: // const promise = Promise.reject("error"); // setTimeout(() => { // promise.catch(() => console.log("The promise is caught")); // }, 1000) if (errors.length > 0) { throw errors[0]; } if (sizeLimitReached) { await deleteUploadedFiles(); return new core_1.HttpResponseBadRequest({ body: { error: 'FILE_SIZE_LIMIT_REACHED', message: `The file "${sizeLimitReached}" is too large. The maximum file size is ${fileSizeLimit} bytes.` } }); } if (numberLimitReached) { await deleteUploadedFiles(); return new core_1.HttpResponseBadRequest({ body: { error: 'FILE_NUMBER_LIMIT_REACHED', message: `Too many files updated. The maximum number of files allowed is ${fileNumberLimit}.` } }); } // Validate the fields const ajv = (0, core_1.getAjvInstance)(); if (!ajv.validate(fieldsSchema, ctx.request.body)) { await deleteUploadedFiles(); return new core_1.HttpResponseBadRequest({ body: ajv.errors }); } // Validate the files for (const name in filesSchema) { if (filesSchema[name].required && ctx.files.get(name).length === 0) { await deleteUploadedFiles(); return new core_1.HttpResponseBadRequest({ body: { error: 'MISSING_FILE', message: `The file "${name}" is required.` } }); } if (!filesSchema[name].multiple && ctx.files.get(name).length > 1) { await deleteUploadedFiles(); return new core_1.HttpResponseBadRequest({ body: { error: 'MULTIPLE_FILES_NOT_ALLOWED', message: `Uploading multiple "${name}" files is not allowed.` } }); } } } const openApiSchema = { ...fieldsSchema, required: fieldsSchema.required ? [...fieldsSchema.required] : [], properties: { ...fieldsSchema.properties } }; for (const key in filesSchema) { const file = filesSchema[key]; if (file.required) { openApiSchema.required.push(key); } if (file.multiple) { openApiSchema.properties[key] = { items: { format: 'binary', type: 'string', }, type: 'array', }; } else { openApiSchema.properties[key] = { format: 'binary', type: 'string', }; } } const requestBody = { content: { 'multipart/form-data': { schema: openApiSchema } } }; const openapi = [ (0, core_1.ApiRequestBody)(requestBody) ]; return (0, core_1.Hook)(hook, openapi, options); }