@nasriya/hypercloud
Version:
Nasriya HyperCloud is a lightweight Node.js HTTP2 framework.
292 lines (291 loc) • 12.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const UploadMemoryFile_1 = __importDefault(require("./UploadMemoryFile"));
const UploadedStorageFile_1 = __importDefault(require("./UploadedStorageFile"));
const requestBody_1 = __importDefault(require("../../handler/assets/requestBody"));
const fs_1 = __importDefault(require("fs"));
const helpers_1 = __importDefault(require("../../../utils/helpers"));
const path_1 = __importDefault(require("path"));
const mimes = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../data/mimes.json'), { encoding: 'utf8' }));
class UploadHandler {
#_currentFile;
#_request;
#_initReq;
#_response;
#_configs = {
/**The maximum file size allowed */
maxFileSize: 0,
// Initialize storage and limits
limits: {
/**The maximum size allowed to store files in memory while uploading */
fileStream: 0,
images: 0,
videos: 0,
mime: null
}
};
#_files = [];
#_fields = {};
#_data = {
contentLength: 0,
boundary: '',
endBoundary: '',
chunks: 0,
};
#_flags = {
useFileStream: false,
finished: false
};
#_promiseResponse = {
resolve: undefined,
reject: undefined,
};
constructor(req, initReq, res) {
this.#_request = req;
this.#_initReq = initReq;
this.#_response = res;
this.#_configs = {
maxFileSize: this.#_request.server.uploads.maxFileSize,
limits: {
fileStream: this.#_request.server.uploads.limits.fileStream,
images: this.#_request.server.uploads.limits.images,
videos: this.#_request.server.uploads.limits.videos,
mime: this.#_request.server.uploads.limits.mime
}
};
}
async #validate() {
if (this.#_request.method !== 'POST') {
throw { code: 500, message: 'Cannot handle form data for a non-POST request' };
}
this.#_data.contentLength = parseInt(this.#_request.headers['content-length'] || '0', 10);
const contentType = this.#_request.headers['content-type'];
if (!contentType || !contentType.includes('multipart/form-data')) {
this.#_response.status(400).json({ message: 'Content-Type must be multipart/form-data' });
throw { code: 400, message: 'Content-Type must be multipart/form-data' };
}
const boundaryMatch = contentType.match(/boundary=(.*)$/);
if (!boundaryMatch?.[1]) {
this.#_response.status(400).json({ message: 'Boundary not found in multipart/form-data' });
throw { code: 400, message: 'Boundary not found in multipart/form-data' };
}
this.#_data.boundary = boundaryMatch[1];
this.#_data.endBoundary = `\r\n--${this.#_data.boundary}--`;
this.#_flags.useFileStream = this.#_data.contentLength > this.#_configs.limits.fileStream;
}
#_helpers = {
analyze: async (data) => {
try {
// Regular expressions to match Content-Disposition and Content-Type headers
const fieldRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)"(?:;\s*filename="[^"]+")?\s*(?:\r?\n|\r?\n|\n)?(?:\r?\n|\r?)([^\r\n]*)/;
const fileRegex = /Content-Disposition:\s*form-data;\s*name="([^"]+)";\s*filename="([^"]+)"\s*(?:\r?\n|\r?\n|\n)?Content-Type:\s*([^;\r\n]*)\s*(?:\r?\n|\r?\n|\n)?(?:\r?\n|\r?)(.*)/;
const matchField = data.match(fieldRegex);
const matchFile = data.match(fileRegex);
if (matchFile) {
// Extract the field name, file name, and content type
const details = {
fieldName: matchFile[1],
fileName: matchFile[2],
mime: matchFile[3],
tempPath: this.#_request.server.uploads.directory
};
if (!details.fieldName || !details.fileName || !details.mime) {
throw new Error(`The header is invalid`);
}
if (!mimes.includes(details.mime)) {
throw new Error(`The request mime type is not supported: ${details.mime}`);
}
this.#_currentFile = (() => {
if (this.#_flags.useFileStream) {
return new UploadedStorageFile_1.default(details);
}
else {
return new UploadMemoryFile_1.default(details);
}
})();
this.#_files.push(this.#_currentFile);
// Remove the matched lines from the data
let cleanedData = data.replace(matchFile[0], '').replace(details.mime, '');
// Remove any leading or trailing newlines caused by the removal
cleanedData = cleanedData.trim();
await this.#_helpers.process(cleanedData);
}
else {
if (matchField) {
this.#_fields[matchField[1]] = matchField[2].trim();
}
}
}
catch (error) {
await this.#_helpers.handleError(error);
}
},
process: async (chunk) => {
if (this.#_flags.useFileStream) {
try {
await this.#_currentFile.write(chunk);
}
catch (error) {
await this.#_helpers.handleError(error);
}
}
else {
this.#_currentFile.push(chunk);
}
},
processNew: async (chunk) => {
await this.#_helpers.finalize();
await this.#_helpers.analyze(chunk);
},
finalize: async () => {
try {
if (this.#_currentFile) {
if (this.#_currentFile instanceof UploadedStorageFile_1.default) {
await this.#_currentFile.finish();
}
this.#_currentFile = undefined;
}
}
catch (error) {
await this.#_helpers.handleError(error);
}
},
cleanUp: async () => {
const promises = this.#_files.filter(file => file instanceof UploadedStorageFile_1.default).map(file => {
return new Promise((resolve, reject) => {
fs_1.default.promises.unlink(file.path).then(() => {
helpers_1.default.printConsole(`Temporary Uploaded File "${file.fileName}" has been deleted from "${file.path}"`);
resolve();
}).catch(err => {
helpers_1.default.printConsole(`Unable to delete Temporary Uploaded File "${file.fileName}" from "${file.path}". Reason: ${err?.message || 'Unknown'}`);
helpers_1.default.printConsole(err);
reject();
});
});
});
const result = await Promise.allSettled(promises);
const rejected = result.filter(i => i.status === 'rejected');
if (rejected.length > 0) {
throw new Error(`Unable to clean uploaded files from "${this.#_request.server.uploads.directory}". Make sure to delete them manually.`);
}
},
handleError: async (error) => {
await this.#_helpers.cleanUp();
this.#_promiseResponse.reject(error);
}
};
#_queue = {
storage: [],
processing: false,
add: (chunk) => {
this.#_data.chunks++;
this.#_queue.storage.push(chunk);
this.#_queue.process();
},
processChunk: async (chunk) => {
try {
const firstChunk = this.#_data.chunks === 1;
const isLastChunk = chunk.includes(this.#_data.endBoundary);
if (isLastChunk) {
chunk = chunk.replace(this.#_data.endBoundary, '');
}
if (chunk.length === 0) {
return;
}
// Normalize the first boundary
if (firstChunk) {
chunk = chunk.replace(`--${this.#_data.boundary}`, this.#_data.boundary);
}
const hasBoundary = chunk.includes(this.#_data.boundary);
if (hasBoundary) {
for (const part of chunk.split(`${this.#_data.boundary}\r\n`).filter(i => i.length > 0)) {
await this.#_helpers.processNew(part);
}
}
else {
await this.#_helpers.process(chunk);
}
if (isLastChunk && this.#_currentFile instanceof UploadedStorageFile_1.default && !this.#_currentFile.closed) {
await this.#_currentFile.finish();
}
}
catch (error) {
throw error;
}
},
process: async () => {
if (this.#_queue.processing) {
return;
}
this.#_queue.processing = true;
try {
while (this.#_queue.storage.length > 0) {
const chunk = this.#_queue.storage.shift();
if (chunk) {
await this.#_queue.processChunk(chunk);
}
}
}
catch (error) {
this.#_queue.processing = false;
await this.#_helpers.handleError(error);
}
this.#_queue.processing = false;
if (this.#_flags.finished)
this.#_queue.onFinished();
},
onFinished: () => {
const storageF = this.#_files.filter(i => i instanceof UploadedStorageFile_1.default).map(file => {
return {
fieldName: file.fieldName,
fileName: file.fileName,
mime: file.mime,
size: file.size,
path: file.path
};
});
const memoryF = this.#_files.filter(i => i instanceof UploadMemoryFile_1.default).map(file => {
return {
fieldName: file.fieldName,
fileName: file.fileName,
mime: file.mime,
size: file.size,
content: file.content
};
});
this.#_initReq.body = new requestBody_1.default().from({
files: [...storageF, ...memoryF],
fields: this.#_fields,
cleanup: this.#_helpers.cleanUp
});
this.#_initReq.bodyType = 'formData';
this.#_helpers.finalize().then(() => this.#_promiseResponse.resolve());
}
};
async handle() {
return new Promise((resolve, reject) => {
this.#_promiseResponse.resolve = resolve;
this.#_promiseResponse.reject = reject;
this.#validate().catch(async (error) => {
await this.#_helpers.handleError(error);
});
this.#_request.httpRequest.on('data', async (chunk) => {
this.#_queue.add(chunk);
});
this.#_request.httpRequest.on('end', async () => {
this.#_flags.finished = true;
if (!this.#_queue.processing) {
this.#_queue.onFinished();
}
});
this.#_request.httpRequest.on('error', async (error) => {
await this.#_helpers.handleError(error);
});
});
}
get contentLength() { return this.#_data.contentLength; }
}
exports.default = UploadHandler;