Skip to content

Instantly share code, notes, and snippets.

@dmshvetsov
Last active December 11, 2020 23:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmshvetsov/b3a0929e58599ce39d4a27cb3223b967 to your computer and use it in GitHub Desktop.
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)
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];
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;
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