@medley/multipart
Version:
Medley plugin for parsing 'multipart/form-data' bodies
189 lines (146 loc) • 4.66 kB
JavaScript
'use strict';
const MultipartError = require('./MultipartError');
const fs = require('fs');
const path = require('path');
const tmpDir = require('os').tmpdir();
var nextReqNum = 0;
class FileAccumulator {
constructor(fileLimits, onError) {
this._fileLimits = fileLimits;
this._handleError = onError;
this._files = {};
this._fileCount = 0;
this._filesFinished = 0;
this._onFilesFinished = null;
this._reqNum = nextReqNum;
nextReqNum = (nextReqNum + 1) % Number.MAX_SAFE_INTEGER;
}
addFile(fieldName, fileStream, fileName, mimeType) {
if (this._fileLimits === null) {
const file = this._saveFile(fieldName, fileStream, fileName, mimeType);
if (this._files[fieldName] === undefined) {
this._files[fieldName] = [file];
} else {
this._files[fieldName].push(file);
}
return null;
}
if (this._fileLimits.hasOwnProperty(fieldName) === false) {
return 'UNEXPECTED_FILE';
}
const maxFiles = this._fileLimits[fieldName];
if (maxFiles < 1) {
return 'LIMIT_FILE_COUNT';
}
if (maxFiles === 1) { // Expecting single file
if (this._files[fieldName] !== undefined) {
return 'LIMIT_FILE_COUNT';
}
this._files[fieldName] = this._saveFile(fieldName, fileStream, fileName, mimeType);
} else { // Expecting array
const filesArray = this._files[fieldName];
if (filesArray === undefined) {
this._files[fieldName] = [this._saveFile(fieldName, fileStream, fileName, mimeType)];
} else {
if (filesArray.length === maxFiles) {
return 'LIMIT_FILE_COUNT';
}
filesArray.push(this._saveFile(fieldName, fileStream, fileName, mimeType));
}
}
return null;
}
_saveFile(fieldName, fileStream, fileName, mimeType) {
const tempPath = this._getTempFilePath(this._reqNum);
const diskStream = fs.createWriteStream(tempPath);
const handleDiskStreamFinish = this._handleFileFinished.bind(this);
diskStream.on('error', this._handleError);
diskStream.on('finish', handleDiskStreamFinish);
fileStream.on('error', this._handleError);
fileStream.on('limit', () => {
this._handleError(new MultipartError('LIMIT_FILE_SIZE', fieldName));
});
fileStream.pipe(diskStream);
return {
fileName,
fileStream,
mimeType,
diskStream,
removeStreamListeners: () => {
fileStream.removeAllListeners('limit');
fileStream.removeListener('error', this._handleError);
diskStream.removeListener('error', this._handleError);
diskStream.removeListener('finish', handleDiskStreamFinish);
},
};
}
_getTempFilePath() {
const fileName = `multipart.${process.pid}-${this._reqNum}-${this._fileCount++}.tmp`;
return path.join(tmpDir, fileName);
}
_handleFileFinished() {
this._filesFinished++;
if (this._filesFinished === this._fileCount && this._onFilesFinished !== null) {
this._onFilesFinished();
}
}
genFiles(cb) {
if (this._filesFinished === this._fileCount) { // Files are already finished
this._genFiles(cb);
} else {
this._onFilesFinished = () => this._genFiles(cb);
}
}
_genFiles(cb) {
const files = Object.create(null);
for (const fieldName in this._files) {
const file = this._files[fieldName];
files[fieldName] = Array.isArray(file)
? file.map(getUserFile)
: getUserFile(file);
}
cb(files);
}
discardFiles() {
for (const fieldName in this._files) {
const file = this._files[fieldName];
if (Array.isArray(file)) {
file.forEach(discardFile);
} else {
discardFile(file);
}
}
this._files = null;
}
}
function getUserFile(file) {
file.removeStreamListeners();
const fileStream = fs.createReadStream(file.diskStream.path);
fileStream.on('close', handleTempFileStreamClose);
return {
stream: fileStream,
fileName: file.fileName,
mimeType: file.mimeType,
size: file.diskStream.bytesWritten,
};
}
function discardFile(file) {
const {fileStream, diskStream} = file;
fileStream.unpipe(diskStream);
file.removeStreamListeners();
if (diskStream.closed) {
fs.unlink(diskStream.path, ignoreErrors);
} else {
diskStream.on('close', handleTempFileStreamClose);
diskStream.end();
}
fileStream.resume();
}
function handleTempFileStreamClose() {
fs.unlink(this.path, ignoreErrors);
this.removeListener('close', handleTempFileStreamClose);
}
function ignoreErrors() {
// Empty function
}
module.exports = FileAccumulator;