Skip to content

Instantly share code, notes, and snippets.

@irvanherz
Last active October 30, 2022 21:16
Show Gist options
  • Save irvanherz/11a0b9fa30d74101c5ea7ca8d099d868 to your computer and use it in GitHub Desktop.
Save irvanherz/11a0b9fa30d74101c5ea7ca8d099d868 to your computer and use it in GitHub Desktop.
Streaming multer storage engine with Sharp image transformation for Minio
/* global Express */
import { Request } from 'express'
import { Client, ClientOptions, ItemBucketMetadata } from 'minio'
import multer from 'multer'
import path from 'path'
import sharp from 'sharp'
type TransformerType = {
id: string
sharp: sharp.Sharp
filename?: (req: Request, file: Express.Multer.File, transformer: TransformerType) => string
meta?: (req: Request, file: Express.Multer.File, transformer: TransformerType) => ItemBucketMetadata
}
type FilenameCallbackType = (req: Request, file: Express.Multer.File, transformer: TransformerType) => string
type ObjectMetaCallbackType = (req: Request, file: Express.Multer.File, transformer: TransformerType) => ItemBucketMetadata
type TransformedType = Partial<Express.Multer.File> & { id: string, status: 'success' | 'error', meta?: any }
type HandleFileCallbackInfoType = Partial<Express.Multer.File> & { transformed: TransformedType[] }
type HandleFileCallbackType = (error?: any, info?: HandleFileCallbackInfoType) => void
type MulterSharpMinioStorageOptions = {
filename?: FilenameCallbackType
meta?: ObjectMetaCallbackType
bucket: string
clientOptions: ClientOptions
transformers: TransformerType[]
}
const defaultFilenameCallback: FilenameCallbackType = (_req, file) => {
const fileExt = path.extname(file.originalname)
return `${Date.now()}${fileExt}`
}
const defaultObjectMetaCallback: ObjectMetaCallbackType = (_req, _file, _transformer) => ({})
class MulterSharpMinioStorage implements multer.StorageEngine {
private filename?: FilenameCallbackType
private meta?: ObjectMetaCallbackType
private bucket: string
private minioClient: Client
private transformers: TransformerType[]
private minioClientOptions: ClientOptions
constructor (opts: MulterSharpMinioStorageOptions) {
this.filename = opts.filename
this.meta = opts.meta
this.bucket = opts.bucket
this.transformers = opts.transformers
this.minioClient = new Client(opts.clientOptions)
this.minioClientOptions = opts.clientOptions
}
_handleFile (
req: Request,
file: Express.Multer.File,
callback: HandleFileCallbackType
) {
const t = this
async function transformation () {
const untransformed = file.stream
const res: TransformedType[] = await Promise.all(
t.transformers.map(async transformer => {
const filenameFn = transformer.filename || t.filename || defaultFilenameCallback
const filename = filenameFn(req, file, transformer)
const media = sharp()
const transformed = sharp()
untransformed.pipe(media).pipe(transformer.sharp).pipe(transformed)
const transformedMetadata = await transformed.metadata()
const objectMetaFn = transformer.meta || t.meta || defaultObjectMetaCallback
const objectMeta = objectMetaFn(req, file, transformer)
return await t.minioClient
.putObject(t.bucket, filename, transformed, objectMeta)
.then(objectInfo => {
const co = t.minioClientOptions
const destination = `${co.useSSL ? 'https' : 'http'}://${co.endPoint}/${t.bucket}/${filename}`
const res: TransformedType = {
id: transformer.id,
status: 'success',
filename,
destination,
fieldname: file.fieldname,
meta: { ...transformedMetadata, objectInfo }
}
return res
})
// eslint-disable-next-line n/handle-callback-err
.catch(err => {
const res: TransformedType = {
id: transformer.id,
status: 'error'
}
return res
})
})
)
return res
}
transformation().then(res => {
callback(null, { transformed: res })
})
}
_removeFile = (_req: Request, file: Express.Multer.File, callback: (error: Error | null) => void): void => {
callback(null)
// NOT YET IMPLEMENTED
// this.minioClient.removeObject(this.bucket, file.filename, () => callback(null))
}
}
export default MulterSharpMinioStorage
// USAGE EXAMPLE:
// import multer from 'multer'
// import path from 'path'
// import sharp from 'sharp'
// import slugify from 'slugify'
// import MulterSharpMinioStorage from '../lib/multer-sharp-minio-storage'
// const upload = multer({
// storage: new MulterSharpMinioStorage({
// filename: (_req, file, t) => {
// const name = path.parse(file.originalname).name
// return slugify(name, { lower: true, trim: true }) + '_' + Date.now() + '_' + t.id + '.jpg'
// },
// meta: (_req, _file, _t) => ({ 'Content-Type': 'image/jpeg' }),
// bucket: process.env.S3_BUCKET,
// transformers: [
// {
// id: '320',
// sharp: sharp().resize(320).jpeg()
// },
// {
// id: '640',
// sharp: sharp().resize(640).jpeg()
// }
// ],
// clientOptions: {
// endPoint: process.env.S3_ENDPOINT,
// accessKey: process.env.S3_ACCESS_KEY_ID,
// secretKey: process.env.S3_SECRET_ACCESS_KEY
// }
// })
// })
// export const uploadImage = upload.single('file')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment