Last active
August 1, 2020 18:37
-
-
Save sudomaxime/c8a5cc2ff4a891b3f7c8ce43c090e90a to your computer and use it in GitHub Desktop.
Example uploader middleware for adminbro
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import express from "express"; | |
import path from "path"; | |
import fs from "fs"; | |
import fileType from "file-type"; | |
import { isArray, promisify } from "util"; | |
interface FileSaveOptions { | |
directory: string | |
buffer: Buffer | |
filename: string | |
extension?: string | |
} | |
interface UploadGuard { | |
name: string, | |
directory?: string, // Defaults to /uploads | |
keepFormidableTemporaryFile?: boolean // defaults to false | |
types?: string[] | |
roles?: string[] | |
multiple?: boolean | |
size?: number, // Children to formidable maxFileSize configuration | |
validator?: (file, conf?: this) => boolean | |
} | |
interface UploadGuardValidation { | |
ok: boolean, | |
status: number, | |
code: string | |
[key: string]: any | |
} | |
const router = express.Router(); | |
const readFile = promisify(fs.readFile); | |
const deleteFile = promisify(fs.unlink); | |
const writeFile = promisify(fs.writeFile); | |
async function saveFile (file: FileSaveOptions) { | |
const dirPath = `${file.directory}/${file.filename}.${file.extension}`; | |
const absPath = path.resolve(__dirname, ".." + dirPath); | |
await writeFile(absPath, file.buffer); | |
return dirPath; | |
} | |
async function handleTmpFile (tmpFilePath, directory): Promise<string> { | |
const buffer = await readFile(tmpFilePath); | |
const filetype = await fileType.fromBuffer(buffer); | |
const extension = filetype?.ext?.toString(); | |
const timestamp = +new Date; | |
const filename = timestamp.toString(); | |
const url = await saveFile({ | |
directory, | |
buffer, | |
filename, | |
extension | |
}); | |
return url; | |
} | |
async function extensionInBufferFromPath (tmpFilePath:string): Promise<string|undefined> { | |
const buffer = await readFile(tmpFilePath); | |
const filetype = await fileType.fromBuffer(buffer); | |
return filetype?.ext?.toString().toLowerCase(); | |
} | |
async function validateUploadGuards (req, guard: UploadGuard): Promise<UploadGuardValidation> { | |
const { files, session } = req; | |
const { types, roles, multiple, size, name, validator } = guard; | |
let ok = false; | |
let status = 400; | |
if (!files[name]) { | |
return {ok, code: "NO_IMAGE_SENT", status}; | |
} | |
const fileExtension = await extensionInBufferFromPath(files[name].path); | |
if (types && !fileExtension) { | |
return {ok, code: "FILE_HAS_NO_MIMETYPE", status}; | |
} | |
if (types && !types.includes(fileExtension!)) { | |
return {ok, code: "FILE_UNAUTHORIZED", types, status}; | |
} | |
if (!multiple && isArray(files[name])) { | |
return {ok, code: "ONLY_SUPPORT_SINGLE_FILE", status}; | |
} | |
if (roles && !roles.includes(session?.adminUser?.role)) { | |
return {ok, code: "UNAUTHORIZED", status: 403}; | |
} | |
// Implementation is safe as we check if multiple is set earlier. | |
if (size && !multiple) { | |
let biggerThanLimit = files[name].size > size; | |
if (biggerThanLimit) { | |
return { | |
ok, | |
code: "OVER_SIZE_LIMIT", | |
status, | |
size, | |
files: [files[name]], | |
} | |
} | |
} | |
// Implementation is safe as we check if multiple is set earlier. | |
if (size && multiple) { | |
let biggerThanLimit = files[name].filter(image => image.size > size); | |
if (biggerThanLimit?.length) { | |
return { | |
ok, | |
code: "OVER_SIZE_LIMIT", | |
status, | |
size, | |
files: biggerThanLimit.map(file => file.name), | |
} | |
} | |
} | |
if (validator) { | |
let isFunction = validator | |
&& {}.toString.call(validator) === '[object Function]'; | |
if (!isFunction) { | |
throw new Error(`Your custom validation for input ${name} should be a function`); | |
} | |
let customValidatorOk = validator(files[name], guard); | |
if (!customValidatorOk) { | |
return { | |
ok: false, | |
code: "CUSTOM_VALIDATOR", | |
validator: validator?.name, | |
status | |
} | |
} | |
} | |
ok = true; | |
return {ok, status: 200, code: "UPLOADED"} | |
} | |
function uploadMiddleware (guards: UploadGuard) { | |
return async (req, res, next) => { | |
let validation = await validateUploadGuards(req, guards); | |
if (!validation.ok) { | |
return res.status(validation.status).send(validation); | |
} | |
// If everything passes, save file and keep url in req. | |
const saveDir = guards.directory || "/uploads" | |
let uploadUrl = await handleTmpFile(req.files.image.path, saveDir); | |
req.uploadUrl = uploadUrl; | |
if (guards.keepFormidableTemporaryFile) { | |
return next(); | |
} | |
// We don't want to wait for this thread to finish | |
// Let node deal with this as a background process. | |
await deleteFile(req.files[guards.name].path); | |
next(); | |
} | |
} | |
router.post("/upload/image", | |
uploadMiddleware({ | |
name: 'image', | |
directory: "/uploads/image", | |
roles: ['admin'], | |
types: ['jpg', 'jpeg', 'png', 'gif'], | |
size: 8_000 * 1_024, // 800kb | |
validator: (file) => { | |
return file && true; | |
} | |
}), | |
// Url is now stored in uploadUrl, the user would do | |
// whatever with this url, store it in DB, send it via email, crop it etc. | |
(req, res) => { | |
res.status(200) | |
res.send({url: req.uploadUrl}) | |
} | |
) | |
export default router; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment