You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
6.1 KiB
228 lines
6.1 KiB
|
3 years ago
|
import { FastifyReply, FastifyRequest } from 'fastify'
|
||
|
|
import is from 'type-is'
|
||
|
|
import { Busboy, BusboyHeaders } from '@fastify/busboy'
|
||
|
|
import extend from 'xtend'
|
||
|
|
import onFinished from 'on-finished'
|
||
|
|
import appendField from 'append-field'
|
||
|
|
|
||
|
|
import Counter from './counter'
|
||
|
|
import MulterError, { ErrorMessages } from './multer-error'
|
||
|
|
import FileAppender from './file-appender'
|
||
|
|
import removeUploadedFiles, { RemoveUploadedFileError } from './remove-uploaded-files'
|
||
|
|
import { Setup, File } from '../interfaces'
|
||
|
|
|
||
|
|
type UploadError = { storageErrors?: RemoveUploadedFileError[] } & Error
|
||
|
|
|
||
|
|
function drainStream(stream: NodeJS.ReadableStream) {
|
||
|
|
stream.on('readable', stream.read.bind(stream))
|
||
|
|
}
|
||
|
|
|
||
|
|
function makePreHandler(setup: Setup) {
|
||
|
|
return function multerPreHandler(
|
||
|
|
request: FastifyRequest,
|
||
|
|
_: FastifyReply,
|
||
|
|
next: (err?: Error) => void,
|
||
|
|
) {
|
||
|
|
const rawRequest = request.raw
|
||
|
|
|
||
|
|
if (!is(rawRequest, ['multipart'])) {
|
||
|
|
return next()
|
||
|
|
}
|
||
|
|
|
||
|
|
const options = setup()
|
||
|
|
|
||
|
|
const limits = options.limits
|
||
|
|
const storage = options.storage
|
||
|
|
const fileFilter = options.fileFilter
|
||
|
|
const fileStrategy = options.fileStrategy
|
||
|
|
const preservePath = options.preservePath
|
||
|
|
|
||
|
|
request.body = Object.create(null)
|
||
|
|
|
||
|
|
let busboy: Busboy
|
||
|
|
|
||
|
|
try {
|
||
|
|
busboy = new Busboy({
|
||
|
|
headers: rawRequest.headers as BusboyHeaders,
|
||
|
|
limits: limits,
|
||
|
|
preservePath: preservePath,
|
||
|
|
})
|
||
|
|
} catch (err) {
|
||
|
|
return next(err as Error)
|
||
|
|
}
|
||
|
|
|
||
|
|
const appender = new FileAppender(fileStrategy, request)
|
||
|
|
let isDone = false
|
||
|
|
let readFinished = false
|
||
|
|
let errorOccured = false
|
||
|
|
const pendingWrites = new Counter()
|
||
|
|
const uploadedFiles: File[] = []
|
||
|
|
|
||
|
|
function done(err?: Error) {
|
||
|
|
if (isDone) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
isDone = true
|
||
|
|
|
||
|
|
rawRequest.unpipe(busboy)
|
||
|
|
drainStream(rawRequest)
|
||
|
|
busboy.removeAllListeners()
|
||
|
|
|
||
|
|
onFinished(rawRequest, function() {
|
||
|
|
next(err)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function indicateDone() {
|
||
|
|
if (readFinished && pendingWrites.isZero() && !errorOccured) {
|
||
|
|
done()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function abortWithError(uploadError: UploadError) {
|
||
|
|
if (errorOccured) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
errorOccured = true
|
||
|
|
|
||
|
|
pendingWrites.onceZero(function() {
|
||
|
|
function remove(file: File, cb: (error?: Error | null) => void) {
|
||
|
|
storage._removeFile(request, file, cb)
|
||
|
|
}
|
||
|
|
|
||
|
|
removeUploadedFiles(uploadedFiles, remove, function(
|
||
|
|
err: Error | null,
|
||
|
|
storageErrors: RemoveUploadedFileError[],
|
||
|
|
) {
|
||
|
|
if (err) {
|
||
|
|
return done(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
uploadError.storageErrors = storageErrors
|
||
|
|
done(uploadError)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function abortWithCode(code: keyof ErrorMessages, optionalField?: string) {
|
||
|
|
abortWithError(new MulterError(code, optionalField))
|
||
|
|
}
|
||
|
|
|
||
|
|
// handle text field data
|
||
|
|
busboy.on('field', function(fieldname, value, fieldnameTruncated, valueTruncated) {
|
||
|
|
if (fieldnameTruncated) {
|
||
|
|
return abortWithCode('LIMIT_FIELD_KEY')
|
||
|
|
}
|
||
|
|
if (valueTruncated) {
|
||
|
|
return abortWithCode('LIMIT_FIELD_VALUE', fieldname)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
|
||
|
|
if (limits && limits.hasOwnProperty('fieldNameSize')) {
|
||
|
|
if (fieldname.length > limits.fieldNameSize!) {
|
||
|
|
return abortWithCode('LIMIT_FIELD_KEY')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
appendField(request.body, fieldname, value)
|
||
|
|
})
|
||
|
|
|
||
|
|
// handle files
|
||
|
|
busboy.on('file', function(fieldname, fileStream, filename, encoding, mimetype) {
|
||
|
|
// don't attach to the files object, if there is no file
|
||
|
|
if (!filename) {
|
||
|
|
return fileStream.resume()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Work around bug in Busboy (https://github.com/mscdex/busboy/issues/6)
|
||
|
|
if (limits && limits.hasOwnProperty('fieldNameSize')) {
|
||
|
|
if (fieldname.length > limits.fieldNameSize!) {
|
||
|
|
return abortWithCode('LIMIT_FIELD_KEY')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const file = {
|
||
|
|
fieldname: fieldname,
|
||
|
|
originalname: filename,
|
||
|
|
encoding: encoding,
|
||
|
|
mimetype: mimetype,
|
||
|
|
}
|
||
|
|
|
||
|
|
const placeholder = appender.insertPlaceholder(file)
|
||
|
|
|
||
|
|
fileFilter(request, file, function(err: UploadError | null, includeFile?: boolean) {
|
||
|
|
if (err) {
|
||
|
|
appender.removePlaceholder(placeholder)
|
||
|
|
return abortWithError(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!includeFile) {
|
||
|
|
appender.removePlaceholder(placeholder)
|
||
|
|
return fileStream.resume()
|
||
|
|
}
|
||
|
|
|
||
|
|
let aborting = false
|
||
|
|
pendingWrites.increment()
|
||
|
|
|
||
|
|
Object.defineProperty(file, 'stream', {
|
||
|
|
configurable: true,
|
||
|
|
enumerable: false,
|
||
|
|
value: fileStream,
|
||
|
|
})
|
||
|
|
|
||
|
|
fileStream.on('error', function(error: Error) {
|
||
|
|
pendingWrites.decrement()
|
||
|
|
abortWithError(error)
|
||
|
|
})
|
||
|
|
|
||
|
|
fileStream.on('limit', function() {
|
||
|
|
aborting = true
|
||
|
|
abortWithCode('LIMIT_FILE_SIZE', fieldname)
|
||
|
|
})
|
||
|
|
|
||
|
|
storage._handleFile(request, file, function(error?: Error | null, info?: Partial<File>) {
|
||
|
|
if (aborting) {
|
||
|
|
appender.removePlaceholder(placeholder)
|
||
|
|
uploadedFiles.push(extend(file, info))
|
||
|
|
return pendingWrites.decrement()
|
||
|
|
}
|
||
|
|
|
||
|
|
if (error) {
|
||
|
|
appender.removePlaceholder(placeholder)
|
||
|
|
pendingWrites.decrement()
|
||
|
|
return abortWithError(error)
|
||
|
|
}
|
||
|
|
|
||
|
|
const fileInfo = extend(file, info)
|
||
|
|
|
||
|
|
appender.replacePlaceholder(placeholder, fileInfo)
|
||
|
|
uploadedFiles.push(fileInfo)
|
||
|
|
pendingWrites.decrement()
|
||
|
|
indicateDone()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
busboy.on('error', function(err: Error) {
|
||
|
|
abortWithError(err)
|
||
|
|
})
|
||
|
|
busboy.on('partsLimit', function() {
|
||
|
|
abortWithCode('LIMIT_PART_COUNT')
|
||
|
|
})
|
||
|
|
busboy.on('filesLimit', function() {
|
||
|
|
abortWithCode('LIMIT_FILE_COUNT')
|
||
|
|
})
|
||
|
|
busboy.on('fieldsLimit', function() {
|
||
|
|
abortWithCode('LIMIT_FIELD_COUNT')
|
||
|
|
})
|
||
|
|
busboy.on('finish', function() {
|
||
|
|
readFinished = true
|
||
|
|
indicateDone()
|
||
|
|
})
|
||
|
|
|
||
|
|
rawRequest.pipe(busboy)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default makePreHandler
|