@foal/storage
Version:
Storage components for FoalTS
173 lines (172 loc) • 6.82 kB
JavaScript
;
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);
}