UNPKG

azure-queue-node

Version:

Simplified and fast Azure Queue Storage client

487 lines (441 loc) 15.9 kB
'use strict'; var request = require('request'); var url = require('url'); var querystring = require('querystring'); var xml2js = require('xml2js'); var versionInfo = require('../package.json').version; var utils = require('./utils'); var XML_PARSER_OPTIONS = { ignoreAttrs: true, trim: true, tagNameProcessors: [xml2js.processors.firstCharLowerCase], explicitArray: false }; var emptyObject = {}; var Client = { // settings object, cannot be edited _settings: null, // request object with defaults for this client _request: null, // decoded azure key _azureKey: null, // retry function _retryLogic: null, _prepareRequestDefaults: function(settings) { var defaults = { encoding: 'utf8', timeout: settings.timeout }; if (settings.agent) { defaults.agent = settings.agent; } if (settings.proxy) { defaults.proxy = settings.proxy; } if (settings.forever === true) { defaults.forever = settings.forever; } if (settings.agentOptions) { defaults.agentOptions = settings.agentOptions; } if (settings.pool != null) { defaults.pool = settings.pool; } return defaults; }, _getRequestSpecificOptions: function _getRequestSpecificOptions(method, path, qs, body) { var now = new Date().toUTCString(); var requestOptions = { method: method, uri: url.parse(this._settings.accountUrl + path), qs: qs, headers: { date: now, 'user-agent': 'azure-queue-node/'+versionInfo, 'content-type': 'application/xml;charset="utf-8"', 'content-length': body ? Buffer.byteLength(body, 'utf8') : 0, 'x-ms-date': now, 'x-ms-version': '2014-02-14' } }; return requestOptions; }, _addSharedKeyAuthHeader: function _addSharedKeyAuthHeader(requestOptions) { var stringToSign = requestOptions.method +'\n'; stringToSign += (requestOptions.headers['content-encoding'] ? requestOptions.headers['content-encoding'] : '') + '\n'; stringToSign += (requestOptions.headers['content-language'] ? requestOptions.headers['content-language'] : '') + '\n'; stringToSign += (requestOptions.headers['content-length'] != null ? requestOptions.headers['content-length'] : '') + '\n'; stringToSign += (requestOptions.headers['content-md5'] ? requestOptions.headers['content-md5'] : '') + '\n'; stringToSign += (requestOptions.headers['content-type'] ? requestOptions.headers['content-type'] : '') + '\n'; //stringToSign += (requestOptions.headers['date'] ? requestOptions.headers['date'] : '') + '\n'; stringToSign += '\n'; // do not count date header (Microsoft's strange logic!) // If-Modified-Since, If-Match, If-None-Match, If-Unmodified-Since, Range are not used, so can be skipped stringToSign += '\n\n\n\n\n'; // CanonicalizedHeaders, only below ones, as currently lib doesn't support queue metadata stringToSign += (requestOptions.headers['x-ms-date'] ? 'x-ms-date:'+requestOptions.headers['x-ms-date']+'\n' : ''); stringToSign += (requestOptions.headers['x-ms-version'] ? 'x-ms-version:'+requestOptions.headers['x-ms-version']+'\n' : ''); // CanonicalizedResource stringToSign += '/'+this._settings.accountName; stringToSign += requestOptions.uri.path; // as possible query string elements are very limited, ifs inlined if (requestOptions.qs) { if ('comp' in requestOptions.qs) { stringToSign += '\ncomp:' + requestOptions.qs.comp; } if ('marker' in requestOptions.qs) { stringToSign += '\nmarker:' + requestOptions.qs.marker; } if ('maxresults' in requestOptions.qs) { stringToSign += '\nmaxresults:' + requestOptions.qs.maxresults; } if ('messagettl' in requestOptions.qs) { stringToSign += '\nmessagettl:' + requestOptions.qs.messagettl; } if ('numofmessages' in requestOptions.qs) { stringToSign += '\nnumofmessages:' + requestOptions.qs.numofmessages; } if ('peekonly' in requestOptions.qs) { stringToSign += '\npeekonly:' + requestOptions.qs.peekonly; } if ('prefix' in requestOptions.qs) { stringToSign += '\nprefix:' + requestOptions.qs.prefix; } if ('popreceipt' in requestOptions.qs) { stringToSign += '\npopreceipt:' + requestOptions.qs.popreceipt; } if ('timeout' in requestOptions.qs) { stringToSign += '\ntimeout:' + requestOptions.qs.timeout; } if ('visibilitytimeout' in requestOptions.qs) { stringToSign += '\nvisibilitytimeout:' + requestOptions.qs.visibilitytimeout; } } requestOptions.headers.authorization = 'SharedKey ' + this._settings.accountName + ':' + utils.hmacSha256(this._azureKey, stringToSign); return requestOptions; }, _normalizeCallback: function _normalizeCallback(cb, error, response, body) { if (error) { return cb(error); } if (!response) { return cb({code: 'UnknownError'}); } // check if response is really XML, if yes, parse it if (body && typeof body === 'string' && body.indexOf('<?xml' === 0)) { xml2js.parseString(body, XML_PARSER_OPTIONS, function(err, result) { if (err) { return cb({code: 'XMLParseError'}); } body = result; next(); }) } else { next(); } function next() { if (response.statusCode >= 400) { return cb({ statusCode: response.statusCode, code: body && body.error ? body.error.code : 'UnknownBody', body: body && body.error ? body.error : body }); } return cb(null, { statusCode: response.statusCode, headers: response.headers, // some important data may be in response headers body: body }); } }, _sendRequestWithRetry: function _sendRequestWithRetry(options, cb) { if (this._retryLogic == null) { this._request(options, this._normalizeCallback.bind(this, cb)); } else { var self = this; this._retryLogic(options, function(filterCb) { self._request(options, self._normalizeCallback.bind(self, function(err, resp) { filterCb(err, resp, function(err, resp) { cb(err, resp); }); })); }); } }, _makeRequest: function _makeRequest(method, path, qs, body, cb) { var options = this._getRequestSpecificOptions(method, path, qs, body); if (this._azureKey) { options = this._addSharedKeyAuthHeader(options); } if (typeof body === 'string' || typeof body === 'object') { options.body = body; } this._sendRequestWithRetry(options, cb); }, create: function create(settings) { if (!settings.accountUrl || !settings.accountName || !settings.accountKey) { throw 'Provide accountUrl, accountName, and accountKey in settings or in env CLOUD_STORAGE_ACCOUNT'; } var sealedSettings = Object.seal(settings); // create request object with most of the default settings var defaultRequest = request.defaults(this._prepareRequestDefaults(sealedSettings)); var retryLogic; if (typeof sealedSettings.retry === 'function') { retryLogic = sealedSettings.retry; } else if (typeof sealedSettings.retry === 'object') { retryLogic = utils.generateRetryFunction(sealedSettings.retry); } else if (sealedSettings.retry === false) { retryLogic = null; } else { retryLogic = utils.generateRetryFunction(); } return Object.create(this, { _settings: {value: sealedSettings}, _request: {value: defaultRequest}, _azureKey: {value: sealedSettings.accountKey ? utils.base64Decode(sealedSettings.accountKey) : null}, _retryLogic: {value: retryLogic} }); }, getSettings: function getSettings() { return this._settings; }, _createQueueCb: function _createQueueCb(cb, options, err, data) { if (!err && (data.statusCode === 201 || data.statusCode === 204)) { return cb(null, data.statusCode === 201); } else { return cb(err); } }, createQueue: function createQueue(queueName, options, cb) { if (typeof options === 'function') { cb = options; } var qs = null; if (typeof options === 'object' && options.timeout) { qs = { timeout: options.timeout }; } this._makeRequest('PUT', queueName, qs, null, this._createQueueCb.bind(this, cb, typeof options === 'object' ? options : null)); return this; }, _listQueuesCb: function _listQueuesCb(cb, err, data) { if (!err && data.statusCode === 200) { var er = data.body.enumerationResults; var results = new Array(er.queues.queue ? er.queues.queue.length || 1 : 0); if (results.length === 1) { results[0] = er.queues.queue.name; } else if (results.length > 1) { er.queues.queue.forEach(function(r, i) { this[i] = r.name; }, results); } var continuation = er.nextMarker ? er.nextMarker : undefined; return cb(null, results, continuation); } else { return cb(err); } }, listQueues: function listQueues(options, cb){ if (typeof options === 'function') { cb = options; } var qs = {comp: 'list'}; if (typeof options === 'object') { if (typeof options.prefix === 'string') { qs.prefix = options.prefix; } if (typeof options.marker === 'string') { qs.marker = options.marker; } if (options.maxResults) { qs.maxresults = options.maxResults; } if (options.timeout) { qs.timeout = options.timeout; } } this._makeRequest('GET', '', qs, null, this._listQueuesCb.bind(this, cb)); return this; }, _204Cb: function _204Cb(cb, err, data) { if (!err && data.statusCode === 204) { return cb(null); } else { return cb(err); } }, deleteQueue: function deleteQueue(queueName, options, cb) { if (typeof options === 'function') { cb = options; } var qs = null; if (typeof options === 'object' && options.timeout) { qs = { timeout: options.timeout }; } this._makeRequest('DELETE', queueName, qs, null, this._204Cb.bind(this, cb)); return this; }, _countMessagesCb: function _countMessagesCb(cb, err, data) { if (!err && data.statusCode === 200) { return cb(null, parseInt(data.headers['x-ms-approximate-messages-count'], 10)); } else { return cb(err); } }, countMessages: function countMessages(queueName, options, cb) { if (typeof options === 'function') { cb = options; } var qs = { comp: 'metadata' }; if (typeof options === 'object' && options.timeout) { qs = { timeout: options.timeout }; } this._makeRequest('HEAD', queueName, qs, null, this._countMessagesCb.bind(this, cb)); return this; }, _putMessageCb: function _putMessageCb(cb, err, data) { if (!err && data.statusCode === 201) { return cb(null); } else { return cb(err); } }, putMessage: function putMessage(queue, message, options, cb) { if (typeof options === 'function') { cb = options; } var qs = null; if (typeof options === 'object') { qs = {}; if (options.visibilityTimeout) { qs.visibilitytimeout = options.visibilityTimeout; } if (options.messageTTL) { qs.messagettl = options.messageTTL; } if (options.timeout) { qs.timeout = options.timeout; } if (Object.keys(qs).length === 0) { qs = null; } } this._makeRequest('POST', queue+'/messages', qs, utils.xmlizeMessage.call(this, options || emptyObject, message), this._putMessageCb.bind(this, cb)); return this; }, clearMessages: function clearMessages(queueName, options, cb) { if (typeof options === 'function') { cb = options; } var qs = null; if (typeof options === 'object' && options.timeout) { qs = { timeout: options.timeout }; } this._makeRequest('DELETE', queueName+'/messages', qs, null, this._204Cb.bind(this, cb)); return this; }, _getMessagesCb: function _getMessagesCb(cb, options, err, data) { if (!err && data.statusCode === 200) { var er = data.body.queueMessagesList; var results = new Array(er.queueMessage ? er.queueMessage.length || 1 : 0); if (results.length === 1) { er.queueMessage = utils.decodeMessage.call(this, options || emptyObject, er.queueMessage); results[0] = er.queueMessage; } else if (results.length > 1) { var self = this; er.queueMessage.forEach(function(r, i) { this[i] = utils.decodeMessage.call(self, options || emptyObject, r); }, results); } return cb(null, results); } else { return cb(err); } }, getMessages: function getMessages(queue, options, cb) { if (typeof options === 'function') { cb = options; } var qs = null; if (typeof options === 'object') { qs = {}; if (options.visibilityTimeout) { qs.visibilitytimeout = options.visibilityTimeout; } if (options.maxMessages) { qs.numofmessages = options.maxMessages; } if (options.timeout) { qs.timeout = options.timeout; } if (Object.keys(qs).length === 0) { qs = null; } } this._makeRequest('GET', queue+'/messages', qs, null, this._getMessagesCb.bind(this, cb, options)); return this; }, peekMessages: function peekMessages(queue, options, cb) { if (typeof options === 'function') { cb = options; } var qs = { peekonly: 'true' }; if (options.maxMessages) { qs.numofmessages = options.maxMessages; } if (options.timeout) { qs.timeout = options.timeout; } // we can use get _getMessagesCb here this._makeRequest('GET', queue+'/messages', qs, null, this._getMessagesCb.bind(this, cb, options)); return this; }, _updateMessageCb: function _updateMessageCb(cb, err, data) { if (!err && data.statusCode === 204) { return cb(null, { popReceipt: data.headers['x-ms-popreceipt'], timeNextVisible: new Date(data.headers['x-ms-time-next-visible']) }); } else { return cb(err); } }, updateMessage: function updateMessage(queue, messageId, popReceipt, visibilityTimeout, message, options, cb) { if (typeof options === 'function') { cb = options; } var qs = { popreceipt: popReceipt, visibilitytimeout: visibilityTimeout }; if (options.timeout) { qs.timeout = options.timeout; } this._makeRequest('PUT', queue+'/messages/'+messageId, qs, utils.xmlizeMessage.call(this, options || emptyObject, message), this._updateMessageCb.bind(this, cb)); return this; }, deleteMessage: function deleteMessage(queue, messageId, popReceipt, options, cb) { if (typeof options === 'function') { cb = options; } var qs = { popreceipt: popReceipt }; if (options.timeout) { qs.timeout = options.timeout; } this._makeRequest('DELETE', queue+'/messages/'+messageId, qs, null, this._204Cb.bind(this, cb)); return this; } }; exports.Client = Client;