Skip to content

Instantly share code, notes, and snippets.

@sudomaxime
Last active August 1, 2020 18:37
Show Gist options
  • Save sudomaxime/c8a5cc2ff4a891b3f7c8ce43c090e90a to your computer and use it in GitHub Desktop.
Save sudomaxime/c8a5cc2ff4a891b3f7c8ce43c090e90a to your computer and use it in GitHub Desktop.
Example uploader middleware for adminbro
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