Skip to content

Instantly share code, notes, and snippets.

@SwapnilSoni1999
Created May 30, 2024 05:57
Show Gist options
  • Save SwapnilSoni1999/c1b96667f67d7c69e357b0d0e596b1ee to your computer and use it in GitHub Desktop.
Save SwapnilSoni1999/c1b96667f67d7c69e357b0d0e596b1ee to your computer and use it in GitHub Desktop.
Universal libmagic file validator, Multer Helper
import fs from 'fs'
import { FileMagic, MagicFlags } from '@npcz/magic'
import { extension } from 'mime-types'
import path from 'path'
import os from 'os'
FileMagic.magicFile = require.resolve(
'@npcz/magic/dist/magic.mgc'
)
if (
process.platform === 'darwin' ||
process.platform === 'linux'
) {
FileMagic.defaultFlags = MagicFlags.MAGIC_PRESERVE_ATIME
}
const getMagic = async () => {
const instance = await FileMagic.getInstance()
return instance
}
const getMimeType = async (
dataOrFilePath: Buffer | string
) => {
let filePath = ''
if (typeof dataOrFilePath === 'string') {
filePath = dataOrFilePath
} else if (dataOrFilePath instanceof Buffer) {
const fileId = Math.random().toString(36).substring(7)
const tempPath = path.join(os.tmpdir(), fileId)
fs.writeFileSync(tempPath, dataOrFilePath)
filePath = tempPath
}
if (!fs.existsSync(filePath)) {
throw new Error('[libmagic] File not found')
}
const magic = await getMagic()
const mimeType = magic.detectMimeType(filePath)
return mimeType
}
const validateMimeType = async (
dataOrFilePath: Buffer | string,
allowedMimeTypes: string[]
): Promise<{
isValid: boolean
mimeType: string
allowedMimeTypes: string[]
}> => {
let filePath = ''
if (typeof dataOrFilePath === 'string') {
filePath = dataOrFilePath
} else if (dataOrFilePath instanceof Buffer) {
const fileId = Math.random().toString(36).substring(7)
const tempPath = path.join(os.tmpdir(), fileId)
fs.writeFileSync(tempPath, dataOrFilePath)
filePath = tempPath
}
const mimeType = await getMimeType(filePath)
const isValid = allowedMimeTypes.includes(mimeType)
if (dataOrFilePath instanceof Buffer) {
fs.unlinkSync(filePath)
}
return { isValid, mimeType, allowedMimeTypes }
}
const getExtension = async (
dataOrMimeType: Buffer | string
) => {
let mimeType = ''
if (typeof dataOrMimeType === 'string') {
mimeType = dataOrMimeType
} else if (dataOrMimeType instanceof Buffer) {
mimeType = await getMimeType(dataOrMimeType)
}
return extension(mimeType)
}
export default {
getMimeType,
validateMimeType,
getExtension
}
import magicLib from '@/lib/magic.lib'
import { NextFunction, Request, Response } from 'express'
interface FileValidationArgs {
field: string
allowedMimeTypes: string[]
strict?: boolean
}
interface FileValidationOptions {
minFieldsRequired?: number
}
const fileValidator = (
args: FileValidationArgs[],
options?: FileValidationOptions
) => {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
const files: Express.Multer.File[] = []
if (req.file) {
files.push(req.file)
}
if (req.files) {
if (Array.isArray(req.files)) {
files.push(...req.files)
} else {
const fields = Object.keys(req.files)
const minRequired =
options?.minFieldsRequired ?? fields.length
console.log(req.files)
console.log({ fields, minRequired })
if (minRequired > fields.length) {
return res.status(400).json({
message: `Any of the ${minRequired} files are required.`,
requiredFields: args.map((item) => ({
field: item.field,
allowedMimeTypes: item.allowedMimeTypes
}))
})
}
for (const { field, strict = true } of args) {
if (req.files[field]) {
const fieldFiles = req.files[field]
const isNotArray = !Array.isArray(fieldFiles)
if (isNotArray && strict) {
return res.status(400).json({
message: `File ${field} is required.`
})
}
if (!isNotArray) {
files.push(...fieldFiles)
}
}
}
}
}
for (const file of files) {
const validation = args.find(
(arg) => arg.field === file.fieldname
)
if (!validation) {
return res
.status(400)
.json({ message: 'Invalid file field' })
}
const result = await magicLib.validateMimeType(
file.buffer,
validation?.allowedMimeTypes
)
if (!result.isValid) {
return res
.status(400)
.json({ message: 'Invalid file type.', result })
}
}
return next()
}
}
export default fileValidator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment