refactor: Client/ServerError on uploadController

This commit is contained in:
Bobby Wibowo 2021-01-08 09:44:04 +07:00
parent a37057e099
commit b5af733dc2
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF

View File

@ -8,7 +8,10 @@ const searchQuery = require('search-query-parser')
const paths = require('./pathsController')
const perms = require('./permissionController')
const utils = require('./utilsController')
const apiErrorsHandler = require('./handlers/apiErrorsHandler.js')
const ClientError = require('./utils/ClientError')
const multerStorage = require('./utils/multerStorage')
const ServerError = require('./utils/ServerError')
const config = require('./../config')
const logger = require('./../logger')
const db = require('knex')(config.database)
@ -110,7 +113,7 @@ const executeMulter = multer({
fileFilter (req, file, cb) {
file.extname = utils.extname(file.originalname)
if (self.isExtensionFiltered(file.extname)) {
return cb(`${file.extname ? `${file.extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`)
return cb(new ClientError(`${file.extname ? `${file.extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`))
}
// Re-map Dropzone keys so people can manually use the API without prepending 'dz'
@ -121,7 +124,7 @@ const executeMulter = multer({
}
if (req.body.chunkindex !== undefined && !chunkedUploads) {
return cb('Chunked uploads are disabled at the moment.')
return cb(new ClientError('Chunked uploads are disabled at the moment.'))
} else {
return cb(null, true)
}
@ -139,7 +142,7 @@ const executeMulter = multer({
})
.catch(error => {
logger.error(error)
return cb('Could not process the chunked upload. Try again?')
return cb(new ServerError('Could not process the chunked upload. Try again?'))
})
} else {
return cb(null, paths.uploads)
@ -219,14 +222,14 @@ self.getUniqueRandomName = async (length, extension) => {
logger.log(`${name} is already in use (${i + 1}/${utils.idMaxTries}).`)
continue
} catch (error) {
// Re-throw error
// Re-throw non-ENOENT error
if (error & error.code !== 'ENOENT') throw error
}
}
return name
}
throw 'Sorry, we could not allocate a unique random name. Try again?'
throw new ServerError('Failed to allocate a unique name for the upload. Try again?')
}
self.parseUploadAge = age => {
@ -253,6 +256,7 @@ self.parseStripTags = stripTags => {
}
self.upload = async (req, res, next) => {
try {
let user
if (config.private === true) {
user = await utils.authorize(req, res)
@ -262,7 +266,7 @@ self.upload = async (req, res, next) => {
.where('token', req.headers.token)
.first()
if (user && (user.enabled === false || user.enabled === 0)) {
return res.json({ success: false, description: 'This account has been disabled.' })
throw new ClientError('This account has been disabled.', { statusCode: 403 })
}
}
@ -273,20 +277,14 @@ self.upload = async (req, res, next) => {
if (temporaryUploads) {
age = self.parseUploadAge(req.headers.age)
if (!age && !config.uploads.temporaryUploadAges.includes(0)) {
return res.json({ success: false, description: 'Permanent uploads are not permitted.' })
throw new ClientError('Permanent uploads are not permitted.', { statusCode: 403 })
}
}
try {
const func = req.body.urls ? self.actuallyUploadUrls : self.actuallyUploadFiles
await func(req, res, user, albumid, age)
} catch (error) {
const isError = error instanceof Error
if (isError) logger.error(error)
return res.status(400).json({
success: false,
description: isError ? error.toString() : error
})
return apiErrorsHandler(error, req, res, next)
}
}
@ -301,14 +299,14 @@ self.actuallyUploadFiles = async (req, res, user, albumid, age) => {
'LIMIT_UNEXPECTED_FILE'
]
if (suppress.includes(error.code)) {
throw error.toString()
throw new ClientError(error.toString())
} else {
throw error
}
}
if (!req.files || !req.files.length) {
throw 'No files.'
throw new ClientError('No files.')
}
// If chunked uploads is enabled and the uploaded file is a chunk, then just say that it was a success
@ -336,32 +334,32 @@ self.actuallyUploadFiles = async (req, res, user, albumid, age) => {
utils.unlinkFile(info.data.filename).catch(logger.error)
))
throw 'Empty files are not allowed.'
throw new ClientError('Empty files are not allowed.')
}
if (utils.clamscan.instance) {
const scanResult = await self.scanFiles(req, user, infoMap)
if (scanResult) throw scanResult
if (scanResult) throw new ClientError(scanResult)
}
await self.stripTags(req, infoMap)
const result = await self.storeFilesToDb(req, res, user, infoMap)
await self.sendUploadResponse(req, res, user, result)
return self.sendUploadResponse(req, res, user, result)
}
self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
if (!config.uploads.urlMaxSize) {
throw 'Upload by URLs is disabled at the moment.'
throw new ClientError('Upload by URLs is disabled at the moment.', { statusCode: 403 })
}
const urls = req.body.urls
if (!urls || !(urls instanceof Array)) {
throw 'Missing "urls" property (array).'
throw new ClientError('Missing "urls" property (array).')
}
if (urls.length > maxFilesPerUpload) {
throw `Maximum ${maxFilesPerUpload} URLs at a time.`
throw new ClientError(`Maximum ${maxFilesPerUpload} URLs at a time.`)
}
const downloaded = []
@ -373,20 +371,16 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
// Extensions filter
let filtered = false
if (['blacklist', 'whitelist'].includes(config.uploads.urlExtensionsFilterMode)) {
if (urlExtensionsFilter) {
if (urlExtensionsFilter && ['blacklist', 'whitelist'].includes(config.uploads.urlExtensionsFilterMode)) {
const match = config.uploads.urlExtensionsFilter.some(extension => extname === extension.toLowerCase())
const whitelist = config.uploads.urlExtensionsFilterMode === 'whitelist'
filtered = ((!whitelist && match) || (whitelist && !match))
} else {
throw 'Invalid extensions filter, please contact the site owner.'
}
} else {
filtered = self.isExtensionFiltered(extname)
}
if (filtered) {
throw `${extname ? `${extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`
throw new ClientError(`${extname ? `${extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`)
}
if (config.uploads.urlProxy) {
@ -425,7 +419,7 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
}))
if (fetchFile.status !== 200) {
throw `${fetchFile.status} ${fetchFile.statusText}`
throw new ServerError(`${fetchFile.status} ${fetchFile.statusText}`)
}
infoMap.push({
@ -448,7 +442,7 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
if (utils.clamscan.instance) {
const scanResult = await self.scanFiles(req, user, infoMap)
if (scanResult) throw scanResult
if (scanResult) throw new ClientError(scanResult)
}
const result = await self.storeFilesToDb(req, res, user, infoMap)
@ -466,17 +460,18 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
const suppress = [
/ over limit:/
]
if (!suppress.some(t => t.test(errorString))) {
throw error
if (suppress.some(t => t.test(errorString))) {
throw new ClientError(errorString)
} else {
throw errorString
throw error
}
}
}
self.finishChunks = async (req, res, next) => {
try {
if (!chunkedUploads) {
return res.json({ success: false, description: 'Chunked upload is disabled at the moment.' })
throw new ClientError('Chunked upload is disabled.', { statusCode: 403 })
}
let user
@ -488,19 +483,13 @@ self.finishChunks = async (req, res, next) => {
.where('token', req.headers.token)
.first()
if (user && (user.enabled === false || user.enabled === 0)) {
return res.json({ success: false, description: 'This account has been disabled.' })
throw new ClientError('This account has been disabled.', { statusCode: 403 })
}
}
try {
await self.actuallyFinishChunks(req, res, user)
} catch (error) {
const isError = error instanceof Error
if (isError) logger.error(error)
return res.status(400).json({
success: false,
description: isError ? error.toString() : error
})
return apiErrorsHandler(error, req, res, next)
}
}
@ -511,7 +500,7 @@ self.actuallyFinishChunks = async (req, res, user) => {
const files = req.body.files
if (!Array.isArray(files) || !files.length || files.some(check)) {
throw 'An unexpected error occurred.'
throw new ClientError('Bad request.')
}
const infoMap = []
@ -521,33 +510,33 @@ self.actuallyFinishChunks = async (req, res, user) => {
chunksData[file.uuid].stream.end()
if (chunksData[file.uuid].chunks > maxChunksCount) {
throw 'Too many chunks.'
throw new ClientError('Too many chunks.')
}
file.extname = typeof file.original === 'string' ? utils.extname(file.original) : ''
if (self.isExtensionFiltered(file.extname)) {
throw `${file.extname ? `${file.extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`
throw new ClientError(`${file.extname ? `${file.extname.substr(1).toUpperCase()} files` : 'Files with no extension'} are not permitted.`)
}
if (temporaryUploads) {
file.age = self.parseUploadAge(file.age)
if (!file.age && !config.uploads.temporaryUploadAges.includes(0)) {
throw 'Permanent uploads are not permitted.'
throw new ClientError('Permanent uploads are not permitted.')
}
}
file.size = chunksData[file.uuid].stream.bytesWritten
if (config.filterEmptyFile && file.size === 0) {
throw 'Empty files are not allowed.'
throw new ClientError('Empty files are not allowed.')
} else if (file.size > maxSizeBytes) {
throw `File too large. Chunks are bigger than ${maxSize} MB.`
throw new ClientError(`File too large. Chunks are bigger than ${maxSize} MB.`)
}
// Double-check file size
const tmpfile = path.join(chunksData[file.uuid].root, chunksData[file.uuid].filename)
const lstat = await paths.lstat(tmpfile)
if (lstat.size !== file.size) {
throw `File size mismatched (${lstat.size} vs. ${file.size}).`
throw new ClientError(`File size mismatched (${lstat.size} vs. ${file.size}).`)
}
// Generate name
@ -586,7 +575,7 @@ self.actuallyFinishChunks = async (req, res, user) => {
if (utils.clamscan.instance) {
const scanResult = await self.scanFiles(req, user, infoMap)
if (scanResult) throw scanResult
if (scanResult) throw new ClientError(scanResult)
}
await self.stripTags(req, infoMap)
@ -615,6 +604,7 @@ self.cleanUpChunks = async (uuid, onTimeout) => {
// Remove tmp file
await paths.unlink(path.join(chunksData[uuid].root, chunksData[uuid].filename))
.catch(error => {
// Re-throw non-ENOENT error
if (error.code !== 'ENOENT') logger.error(error)
})
@ -799,7 +789,7 @@ self.storeFilesToDb = async (req, res, user, infoMap) => {
self.sendUploadResponse = async (req, res, user, result) => {
// Send response
res.json({
return res.json({
success: true,
files: result.map(file => {
const map = {
@ -832,7 +822,7 @@ self.sendUploadResponse = async (req, res, user, result) => {
})
}
self.delete = async (req, res) => {
self.delete = async (req, res, next) => {
// Map /api/delete requests to /api/bulkdelete
let body
if (req.method === 'POST') {
@ -852,10 +842,11 @@ self.delete = async (req, res) => {
} */
req.body = body
return self.bulkDelete(req, res)
return self.bulkDelete(req, res, next)
}
self.bulkDelete = async (req, res) => {
self.bulkDelete = async (req, res, next) => {
try {
const user = await utils.authorize(req, res)
if (!user) return
@ -863,19 +854,18 @@ self.bulkDelete = async (req, res) => {
const values = req.body.values
if (!Array.isArray(values) || !values.length) {
return res.json({ success: false, description: 'No array of files specified.' })
throw new ClientError('No array of files specified.')
}
try {
const failed = await utils.bulkDeleteFromDb(field, values, user)
return res.json({ success: true, failed })
await res.json({ success: true, failed })
} catch (error) {
logger.error(error)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
return apiErrorsHandler(error, req, res, next)
}
}
self.list = async (req, res) => {
self.list = async (req, res, next) => {
try {
const user = await utils.authorize(req, res)
if (!user) return
@ -994,20 +984,14 @@ self.list = async (req, res) => {
// Regular user threshold check
if (!ismoderator && textQueries > MAX_TEXT_QUERIES) {
return res.json({
success: false,
description: `Users are only allowed to use ${MAX_TEXT_QUERIES} non-keyed keyword${MAX_TEXT_QUERIES === 1 ? '' : 's'} at a time.`
})
throw new ClientError(`Users are only allowed to use ${MAX_TEXT_QUERIES} non-keyed keyword${MAX_TEXT_QUERIES === 1 ? '' : 's'} at a time.`)
}
if (filterObj.queries.text) {
for (let i = 0; i < filterObj.queries.text.length; i++) {
const result = sqlLikeParser(filterObj.queries.text[i])
if (!ismoderator && result.count > MAX_WILDCARDS_IN_KEY) {
return res.json({
success: false,
description: `Users are only allowed to use ${MAX_WILDCARDS_IN_KEY} wildcard${MAX_WILDCARDS_IN_KEY === 1 ? '' : 's'} per key.`
})
throw new ClientError(`Users are only allowed to use ${MAX_WILDCARDS_IN_KEY} wildcard${MAX_WILDCARDS_IN_KEY === 1 ? '' : 's'} per key.`)
}
filterObj.queries.text[i] = result.escaped
}
@ -1017,10 +1001,7 @@ self.list = async (req, res) => {
for (let i = 0; i < filterObj.queries.exclude.text.length; i++) {
const result = sqlLikeParser(filterObj.queries.exclude.text[i])
if (!ismoderator && result.count > MAX_WILDCARDS_IN_KEY) {
return res.json({
success: false,
description: `Users are only allowed to use ${MAX_WILDCARDS_IN_KEY} wildcard${MAX_WILDCARDS_IN_KEY === 1 ? '' : 's'} per key.`
})
throw new ClientError(`Users are only allowed to use ${MAX_WILDCARDS_IN_KEY} wildcard${MAX_WILDCARDS_IN_KEY === 1 ? '' : 's'} per key.`)
}
filterObj.queries.exclude.text[i] = result.escaped
}
@ -1135,10 +1116,7 @@ self.list = async (req, res) => {
return !uploaders.find(uploader => uploader.username === username)
})
if (notFound) {
return res.json({
success: false,
description: `User${notFound.length === 1 ? '' : 's'} not found: ${notFound.join(', ')}.`
})
throw new ClientError(`User${notFound.length === 1 ? '' : 's'} not found: ${notFound.join(', ')}.`)
}
}
@ -1178,10 +1156,7 @@ self.list = async (req, res) => {
if (!allowed.includes(column)) {
// Alert users if using disallowed/missing columns
return res.json({
success: false,
description: `Column \`${column}\` cannot be used for sorting.\n\nTry the following instead:\n${allowed.join(', ')}`
})
throw new ClientError(`Column "${column}" cannot be used for sorting.\n\nTry the following instead:\n${allowed.join(', ')}`)
}
sortObj.parsed.push({
@ -1194,10 +1169,7 @@ self.list = async (req, res) => {
// Regular user threshold check
if (!ismoderator && sortObj.parsed.length > MAX_SORT_KEYS) {
return res.json({
success: false,
description: `Users are only allowed to use ${MAX_SORT_KEYS} sort key${MAX_SORT_KEYS === 1 ? '' : 's'} at a time.`
})
throw new ClientError(`Users are only allowed to use ${MAX_SORT_KEYS} sort key${MAX_SORT_KEYS === 1 ? '' : 's'} at a time.`)
}
// Delete key to avoid unexpected behavior
@ -1216,10 +1188,7 @@ self.list = async (req, res) => {
if (inQuery || inExclude) {
filterObj.flags[`is${type}`] = inExclude ? false : inQuery
if (isLast !== undefined && isLast !== filterObj.flags[`is${type}`]) {
return res.json({
success: false,
description: 'Cannot mix inclusion and exclusion type-is keys.'
})
throw new ClientError('Cannot mix inclusion and exclusion type-is keys.')
}
isKeys++
isLast = filterObj.flags[`is${type}`]
@ -1233,10 +1202,7 @@ self.list = async (req, res) => {
// Regular user threshold check
if (!ismoderator && isKeys > MAX_IS_KEYS) {
return res.json({
success: false,
description: `Users are only allowed to use ${MAX_IS_KEYS} type-is key${MAX_IS_KEYS === 1 ? '' : 's'} at a time.`
})
throw new ClientError(`Users are only allowed to use ${MAX_IS_KEYS} type-is key${MAX_IS_KEYS === 1 ? '' : 's'} at a time.`)
}
}
@ -1376,7 +1342,6 @@ self.list = async (req, res) => {
})
}
try {
// Query uploads count for pagination
const count = await db.table('files')
.where(filter)
@ -1477,10 +1442,9 @@ self.list = async (req, res) => {
users[user.id] = user.username
}
return res.json({ success: true, files, count, users, albums, basedomain })
await res.json({ success: true, files, count, users, albums, basedomain })
} catch (error) {
logger.error(error)
return res.status(500).json({ success: false, description: 'An unexpected error occurred. Try again?' })
return apiErrorsHandler(error, req, res, next)
}
}