Last active
December 11, 2020 23:26
-
-
Save dmshvetsov/b3a0929e58599ce39d4a27cb3223b967 to your computer and use it in GitHub Desktop.
Rewrite of multer-azure-storage package with additional helper function (used in Express app)
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 { resolve } from 'path'; | |
import fs from 'fs'; | |
import { createBlobService } from 'azure-storage'; | |
import config from 'config'; | |
/** | |
* Service to handle files downloads requests | |
* | |
*/ | |
const uploadsPath = resolve(__dirname, `../../../${config.uploadsDir}`); | |
/** | |
* Disk storage strategy | |
* | |
* @param {String} collection – name of directory to upload | |
* @param {String} filename – file name | |
* @param {Response} res – express Response | |
* @param {Function} cb – callback on err or success (if error occur cb(err) will be invoked, else cb()) | |
*/ | |
function diskStorage (collection, filename, res, cb) { | |
const blobPath = `${uploadsPath}/${collection}/${filename}`; | |
res.setHeader('X-Content-Type-Options', 'sniff'); | |
const fileStream = fs.createReadStream(blobPath); | |
fileStream.on('error', cb); | |
fileStream.on('finish', cb); | |
fileStream.pipe(res); | |
} | |
/** | |
* Azure storage strategy | |
* | |
* @param {String} collection – name of directory to upload | |
* @param {String} filename – file name | |
* @param {Response} res – express Response | |
* @param {Function} cb – callback on err or success (if error occur cb(err) will be invoked, else cb()) | |
*/ | |
function cloudStorage (collection, filename, res, cb) { | |
const blobStorage = createBlobService(config.azureStorage.connectionString); | |
const blobPath = `${config.uploadsDir}/${collection}/${filename}`; | |
res.setHeader('X-Content-Type-Options', 'sniff'); | |
const stream = blobStorage.createReadStream(config.azureStorage.container, blobPath); | |
stream.on('error', cb); | |
stream.on('finish', cb); | |
stream.pipe(res); | |
} | |
/** | |
* Test storage (stub) | |
* | |
* @param {String} collection – name of directory to upload | |
* @param {String} filename – file name | |
* @param {Response} res – express Response | |
* @param {Function} cb – callback on err or success (if error occur cb(err) will be invoked, else cb()) | |
*/ | |
function fakeStorage (collection, filename, res, cb) { | |
// Do nothing | |
} | |
// Map of storage strategies to environments of the service | |
const strategies = { | |
production: cloudStorage, | |
staging: cloudStorage, | |
development: diskStorage, | |
test: fakeStorage | |
}; | |
export default strategies[config.env]; |
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 config from 'config'; | |
import Multer from 'multer'; | |
import { ValidationError } from 'utils/errors'; | |
import { v4 as uuid } from 'uuid'; | |
/** | |
* Service to handle files uploads | |
* | |
*/ | |
// 1 MB in bytes | |
const MB = 1024 * 1024; | |
// Allowed File Types for uploads | |
const fileTypesWhiteList = Object.freeze([ | |
'pdf', | |
'eml', | |
'txt', | |
'docx', | |
'doc', | |
'xlsx', | |
'xls', | |
'pptx', | |
'ppt', | |
'tiff', | |
'jpg', | |
'jpeg', | |
'png' | |
]); | |
const fileTypeRegExp = new RegExp(fileTypesWhiteList.join('|')); | |
/** | |
* Accept or decline files | |
* | |
* @param {Express.Request} req - file upload request | |
* @param {*} file - multer reference to file | |
* @param {Function} callback - callback function, expects null or error and boolean | |
* accept or decline the file | |
* @return {*} result of the callback function | |
*/ | |
function fileFilter (req, file, callback) { | |
if (fileTypeRegExp.test(file.mimetype)) { | |
return callback(null, true); | |
} | |
const err = new ValidationError( | |
`Unsupported file type uploaded \`${file.mimetype}\`, allowed types ${fileTypesWhiteList.join(', ')}` | |
); | |
return callback(err); | |
} | |
/** | |
* Store uploads on Azure cloud storage service | |
* | |
* @param {String} collection - name of the collection that the upload is related | |
* @param {Object} config - app config object | |
* @param {Function} filenameFn - callback function for multer filename | |
* @return {multer} hard drive storage | |
*/ | |
function cloudStorage (collection, config, filenameFn) { | |
function blobPathResolver (req, file, cb) { | |
filenameFn(req, file, (err, filename) => | |
cb(err, `${config.uploadsDir}/${collection}/${filename}`) | |
); | |
} | |
return new MulterAzureStorage({ | |
connectionString: config.azureStorage.connectionString, | |
container: config.azureStorage.container, | |
blobPathResolver | |
}); | |
} | |
/** | |
* Store uploads to hard drive on which the service is running | |
* | |
* @param {String} collection - name of the collection that the upload is related | |
* @param {Object} config - app config object | |
* @param {Function} filenameFn - callback function for multer filename | |
* @return {multer} hard drive storage | |
*/ | |
export default function diskStorage (collection, config, filenameFn) { | |
// set req.file as | |
// { mimetype, encoding, originalname, path, filename, size } | |
return multer.diskStorage({ | |
destination (req, file, cb) { | |
cb(null, `${config.uploadsDir}/${collection}`); | |
}, | |
filename: filenameFn | |
}); | |
} | |
/** | |
* Store uploads in memory on which the service is running | |
* | |
* @param {Object} config - app config object | |
* @param {Function} filenameFn - callback function for multer filename | |
* @return {multer} memory storage | |
*/ | |
function memoryStorage (config, filenameFn) { | |
return multer.memoryStorage(); | |
} | |
// Map of storage strategies to environments of the service | |
const strategies = { | |
production: cloudStorage, | |
staging: cloudStorage, | |
development: diskStorage, | |
test: memoryStorage | |
}; | |
/** | |
* Upload service initialization function | |
* | |
* @param {String} collection – name of a DB collection that the uploads is related to | |
* @param {String} id – id of the record that the upload is related to | |
* @returns {Function} – instance of Multer class, the service | |
*/ | |
function upload (collection, id) { | |
function filename ({ params }, file, cb) { | |
cb(null, `${id}--${uuid()}--${file.originalname}`); | |
} | |
return Multer({ | |
storage: strategies[config.env](collection, config, filename), | |
limits: { fileSize: 10 * MB }, | |
fileFilter | |
}); | |
} | |
module.exports = upload; |
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 azureStorage from 'azure-storage'; | |
/** | |
* Wrapper for azure-storage npm package | |
* | |
* {@link https://github.com/Azure/azure-storage-node#microsoft-azure-storage-sdk-for-nodejs-and-javascript-for-browsers | azure-storage:Docs} | |
* @constructor | |
* @param {Object} opts options | |
*/ | |
function Blob (opts) { | |
this.container = opts.container; | |
this.blobSrv = azureStorage.createBlobService(opts.connectionString); | |
this.createContainer(this.container); | |
this.blobPathResolver = opts.blobPathResolver; | |
this.storageOptions = opts.storageOptions || {}; | |
} | |
/** | |
* Method can be used to create a container in which to store a blob | |
* {@link https://github.com/Azure/azure-storage-node#blob-storage | azure-storage:Docs} | |
* | |
* @param {String} name container name | |
*/ | |
Blob.prototype.createContainer = function createContainer (name) { | |
this.blobSrv.createContainerIfNotExists(name, (err, result, response) => { | |
if (err) { | |
throw err; | |
} | |
}); | |
}; | |
/** | |
* Upload file to storage | |
* | |
* @param {Request} req - express Request | |
* @param {Object} file - file object from browser (client) | |
* @param {Function} cb - callback | |
* @returns {Function} wrapped function for upload | |
*/ | |
Blob.prototype.uploadToBlob = function uploadToBlob (req, file, cb) { | |
return (storageOpts, blobPath) => { | |
const options = storageOpts || this.storageOptions; | |
const blobStream = this.blobSrv.createWriteStreamToBlockBlob(this.container, blobPath, options, (err) => { | |
if (err) { | |
return cb(err) | |
} | |
return true; | |
}); | |
file.stream.pipe(blobStream); | |
blobStream.on('close', () => { | |
this.blobSrv.getBlobProperties(this.container, blobPath, (err, result) => { | |
if (err) { | |
return cb(err); | |
} | |
const fullUrl = this.blobSrv.getUrl(this.container, blobPath); | |
const fileClone = JSON.parse(JSON.stringify(file)); | |
fileClone.container = this.container; | |
fileClone.path = blobPath; | |
fileClone.url = fullUrl; | |
fileClone.size = parseInt(result.contentLength, 10); | |
fileClone.mimetype = file.mimetype; | |
return cb(null, fileClone); | |
}); | |
}); | |
blobStream.on('error', (err) => { | |
cb(err); | |
}); | |
}; | |
}; | |
/** | |
* Pre upload file processing | |
* | |
* @param {Request} req - express Request | |
* @param {Object} file - file info object | |
* @param {Function} cb - callback | |
* @private | |
*/ | |
Blob.prototype._handleFile = function _handleFile (req, file, cb) { | |
this.blobPathResolver(req, file, this.uploadToBlob(req, file, cb)); | |
}; | |
/** | |
* Remove file from storage | |
* @param {Request} req - express Request | |
* @param {Object} file - file info object | |
* @param {Function} cb - callback after delete or error | |
* @private | |
*/ | |
Blob.prototype._removeFile = function _removeFile (req, file, cb) { | |
this.blobSrv.deleteBlob(this.container, file.path, cb); | |
}; | |
export default Blob; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment