Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

# v0.0.1 is coming

  • Loading branch information...
commit e4e61b84df9fab0b5e4a324b6f05112a4fac6d14 0 parents
Juan Pablo Garcia Dalolla authored
7 .gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+lib-cov
+**.swp
+*.swo
+*.swn
+node_modules/
+examples_local/
3  History.md
@@ -0,0 +1,3 @@
+0.0.1 / 2011-07-31
+==================
+* Initial Release: Basic support for Blobs
22 LICENSE
@@ -0,0 +1,22 @@
+THE MIT LICENSE
+
+Copyright (c) 2010 by Juan Pablo Garcia Dalolla
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 Makefile
@@ -0,0 +1,11 @@
+PREFIX ?= /usr/local
+BIN = ./node_modules/expresso/bin/expresso
+JSCOV = ./node_modules/expresso/deps/jscoverage/node-jscoverage
+
+test: $(BIN)
+ @./$(BIN) -I lib --growl $(TEST_FLAGS) test/**/*.test.js
+
+test-cov:
+ @./$(BIN) -I lib --cov $(TEST_FLAGS) test/**/*.test.js
+
+.PHONY: test test-cov
118 Readme.md
@@ -0,0 +1,118 @@
+# Windows Azure Storage library for Node.js
+A simple implementation of Windows Azure Storage API for Node.js
+
+# Installation
+waz-storage-js depends on querystring (>= v0.0.1) and xml2js (>= v0.1.9).
+
+To install via npm
+
+ npm install waz-storage-js
+
+# Examples
+
+## Initial Setup
+
+ var waz = waz.establishConnection({
+ accountName: 'your_account_name'
+ , accountKey: 'your_key',
+ , useSsl: false
+ });
+
+## Blobs
+
+ // Creating a new container
+ waz.blobs.container.create('myContainer', function(err, result){
+ });
+
+ // Listing existing containers
+ waz.blobs.container.list(function(err, result){
+ });
+
+ // Finding a container
+ waz.blobs.container.find('myContainer', function(err, container){
+
+ // Getting container's metadata
+ container.metadata(function(err, metadata){
+ });
+
+ // Adding properties to a container
+ container.putProperties({'x-ms-custom' : 'MyValue'}, function(err, result){
+ });
+
+ // Getting container's ACL
+ container.getAcl(function(err, result){
+ });
+
+ // Setting container's ACL (null, 'blob', 'container')
+ container.setAcl('container', function(err, result){
+ });
+
+ // Listing blobs in a container
+ container.blobs(function(err, result){
+ });
+
+ // Getting blob's information
+ result.getBlob('myfolder/my file.txt', function(err, result){
+ });
+
+ // Uploading a new Blob
+ result.store('folder/my file.xml', '<xml>content</xml>', 'text/xml', {'x-ms-MyProperty': 'value'}, function(err, result){
+ });
+ }
+ });
+
+ // Deleting containers
+ waz.blobs.container.delete('myContainer', function(err){
+ });
+
+## Queues
+ Coming Soon - If you are anxious, you can always contribute with the project :)
+
+## Tables
+ Coming Soon - If you are anxious, you can always contribute with the project :)
+
+
+# Remaining Stuff
+
+## Blobs
+* Blocks
+* Snapshots
+
+## Queues
+* Everything
+
+## Tables
+* Everything
+
+# Known Issues
+
+* Container's putProperties doesn't work.
+
+# Authors
+
+* Juan Pablo Garcia ([jpgarcia](http://github.com/jpgarcia) | [@jpgd](http://www.twitter.com/jpgd))
+
+# License
+
+(The MIT License)
+
+Copyright (c) 2010 by Juan Pablo Garcia Dalolla
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
60 examples/blobs.js
@@ -0,0 +1,60 @@
+var waz = require('../');
+
+waz.establishConnection( { accountName: 'your_account_name', accountKey: 'your_account_key', useSsl: false } );
+
+waz.blobs.container.create('test1', function(err, result){
+ console.log('\n_________| creating a container |_________\n');
+ console.log(result || err);
+
+ waz.blobs.container.create('test2', function(err, result){
+ console.log('\n_________| creating a container |_________\n');
+ console.log(result || err);
+
+ waz.blobs.container.list(function(err, result){
+ console.log('\n_________| listing existing containers |_________\n');
+ console.log(result || err.message);
+
+ result[0].putProperties({'x-ms-Custom' : 'MyValue'}, function(err, result){
+ console.log(result);
+ });
+
+ waz.blobs.container.find('test1', function(err, result){
+ console.log('\n_________| finding a container |_________\n');
+ console.log(result || err);
+
+ result.blobs(function(err, blobs){
+ console.log(blobs)
+ });
+
+ result.store('Folder/hello world.xml', '<xml/>', 'text/xml', {'x-ms-test': 'myvalue'}, function(err, result){
+ console.log(result);
+ });
+
+ result.getBlob('Folder/hello world.xml', function(err, result){
+ console.log(result);
+ });
+
+ result.setAcl('container', function(err, result){
+ result.getAcl(function(err, result){
+ console.log(result);
+ });
+ });
+
+ result.metadata(function(err, result){
+ console.log('\n_________| showing container metadata |_________\n');
+ console.log(err || result);
+ });
+
+ waz.blobs.container.delete('test1', function(err){
+ console.log('\n_________| removing a container |_________\n');
+ console.log(err || 'test1 container removed!');
+ });
+
+ waz.blobs.container.delete('test2', function(err){
+ console.log('\n_________| removing a container |_________\n');
+ console.log(err || 'test2 container removed!');
+ });
+ });
+ });
+ });
+});
1  index.js
@@ -0,0 +1 @@
+module.exports = require('./lib/waz-storage');
13 lib/waz-blobs/blob.js
@@ -0,0 +1,13 @@
+var utils = require('../waz-storage/utils');
+
+var serviceInstance;
+
+var Blob = module.exports = exports = function Blob(options) {
+ this.name = options.name;
+ this.url = options.url;
+ this.contentType = options.contentType;
+ this.path = this.url.replace(/https?:\/\/[^\/]+\//i, '').match(/([^&]+)/i)[1]
+
+ serviceInstance = options.serviceInstance;
+}
+
130 lib/waz-blobs/container.js
@@ -0,0 +1,130 @@
+var utils = require('../waz-storage/utils')
+ , Blob = require('./blob');
+
+var serviceInstance;
+
+var Container = module.exports = exports = function Container(options) {
+ // TODO: validate that a name is provided
+ this.name = options.name;
+ this.url = options.url;
+ this.lastModified = options.lastModified;
+ serviceInstance = options.serviceInstance;
+}
+
+Container.prototype.metadata = function(callback) {
+ serviceInstance.getContainerProperties(this.name, function(err, data) {
+ callback(err, data);
+ });
+};
+
+Container.prototype.putProperties = function(properties, callback) {
+ serviceInstance.setContainerProperties(this.name, properties, function(err, data) {
+ callback(err, data);
+ });
+};
+
+Container.prototype.getAcl = function(callback) {
+ serviceInstance.getContainerAcl(this.name, function(err, data) {
+ if (data == null) data = 'None';
+ callback(err, data);
+ });
+};
+
+Container.prototype.setAcl = function(level, callback) {
+ var container = this;
+ serviceInstance.setContainerAcl(this.name, level, function(err, data) {
+ callback(err, container);
+ });
+};
+
+Container.prototype.blobs = function(callback) {
+ serviceInstance.listBlobs(this.name, function(err, data) {
+ var blobs = data.map(function(b){ return new Blob(b) });
+ callback(err, blobs);
+ });
+};
+
+Container.prototype.getBlob = function(name, callback) {
+ var blobName = escape(name.replace(/^\//,''));
+ var path = this.name + '/' + blobName;
+
+ serviceInstance.getBlobProperties(path, function(err, data) {
+ // TODO: error handling
+ var url = serviceInstance.generateRequestUri(path, {});
+ var blob = new Blob( { name: name, contentType: data['content-type'], url: url});
+ callback(err, blob);
+ });
+};
+
+Container.prototype.store = function(name, payload, contentType, metadata, callback) {
+ var blobName = escape(name.replace(/^\//,''));
+ var path = this.name + '/' + blobName;
+ contentType = contentType || "application/octet-stream";
+ serviceInstance.putBlob(path, payload, contentType, metadata, function(err, data) {
+ // TODO: error handling
+ var url = serviceInstance.generateRequestUri(path, {});
+ var blob = new Blob( { name: name, contentType: contentType, url: url});
+ callback(err, blob);
+ });
+};
+
+exports.Service = require('./service');
+
+exports.load = function(base) {
+ options = base.defaultConnection.merge( { typeOfService: "blob"} );
+ this.serviceInstance = new this.Service(options);
+ return this;
+}
+
+exports.list = function(callback) {
+ var options = { serviceInstance: this.serviceInstance };
+
+ this.serviceInstance.listContainers(options, function(err, data) {
+ var result = null;
+
+ if (!err) result = data.map(function(item) {
+ options = options.merge(item);
+ return new Container(options);
+ });
+
+ callback(err, result);
+ });
+}
+
+exports.create = function(name, callback) {
+ // TODO: validate name format.
+ var options = { serviceInstance: this.serviceInstance };
+
+ this.serviceInstance.createContainer(name, function(err, data) {
+ var result = null;
+
+ if (!err) {
+ options = options.merge(data);
+ result = new Container(options);
+ }
+
+ callback(err, result);
+ });
+}
+
+exports.delete = function(name, callback) {
+ this.serviceInstance.deleteContainer(name, function(err) {
+ callback(err, null);
+ });
+}
+
+exports.find = function(name, callback) {
+ var options = { serviceInstance: this.serviceInstance };
+
+ this.serviceInstance.getContainerProperties(name, function(err, data) {
+ var result = null
+
+ if (!err) {
+ options = options.merge({ name: name });
+ options = options.merge(data);
+ result = new Container(options);
+ }
+
+ callback(err, result);
+ });
+}
4 lib/waz-blobs/index.js
@@ -0,0 +1,4 @@
+exports.load = function(base) {
+ exports.container = require('./container').load(base);
+ return this;
+}
162 lib/waz-blobs/service.js
@@ -0,0 +1,162 @@
+var utils = require('../waz-storage/utils')
+ , xml2js = require('xml2js')
+ , CoreService = require('../waz-storage/core-service');
+
+exports = module.exports = Service;
+
+function Service(options) {
+ this.options = options;
+ this.coreService = new CoreService(options);
+}
+
+Service.prototype.listContainers = function(args, callback){
+ var options = {comp: 'list'};
+
+ this.coreService.execute('get', null, options, null, null, function(err, response) {
+ if (err != null && err.statusCode == 404) {
+ callback({ message: 'container `' + name + '` not found' }, null);
+ return;
+ }
+
+ var parser = new xml2js.Parser();
+ parser.addListener('end', function(result) {
+ var containers = [];
+
+ if (result.Containers.Container)
+ containers = [result.Containers.Container].flatten().map(function(c){ return { name: c.Name, url: c.Url, lastModified: c.LastModified }; });
+
+ callback(null, containers);
+ });
+
+ var result = parser.parseString(response.body);
+ });
+};
+
+Service.prototype.listBlobs = function(containerName, callback){
+ var options = {restype: 'container', comp: 'list'};
+
+ this.coreService.execute('get', containerName, options, {'x-ms-version': '2009-09-19'}, null, function(err, response) {
+ var parser = new xml2js.Parser();
+
+ parser.addListener('end', function(result) {
+ var blobs = [];
+
+ if (result.Blobs.Blob)
+ blobs = [result.Blobs.Blob].flatten().map(function(c){ return { name: c.Name, url: c.Url, contentType: c.Properties['Content-Type'] }; });
+
+ callback(null, blobs);
+ });
+
+ var result = parser.parseString(response.body);
+ });
+};
+
+Service.prototype.createContainer = function(name, callback){
+ this.coreService.execute('put', name, {restype: 'container'}, {'x-ms-version': '2009-09-19'}, null, function(err, response) {
+ var error = null, data = null
+
+ if (err != null && err.statusCode == 409)
+ error = { message: 'container `' + name + '` already exists' };
+ else
+ data = { name: name };
+
+ callback(error, data);
+ });
+};
+
+Service.prototype.deleteContainer = function(name, callback){
+ this.coreService.execute('delete', name, {restype: 'container'}, {'x-ms-version': '2009-09-19'}, null, function(err, response) {
+ var error = null, data = null
+
+ if (err != null && err.statusCode == 404)
+ error = { message: 'container `' + name + '` not found' };
+
+ callback(error, data);
+ });
+};
+
+Service.prototype.getContainerProperties = function(name, callback){
+ this.coreService.execute('get', name, {restype: 'container'}, {'x-ms-version': '2009-09-19'}, null, function(err, response) {
+ var error = null, data = null
+
+ if (err != null && err.statusCode == 404)
+ error = { message: 'container `' + name + '` not found' };
+ else
+ data = response.headers;
+
+ callback(error, data);
+ });
+};
+
+Service.prototype.setContainerProperties = function(name, properties, callback){
+ this.coreService.execute('put', name, {restype: 'container', comp: 'metadata'}, {'x-ms-version': '2009-09-19'}.merge(properties || {}), null, function(err, response) {
+ var error = null, data = null
+
+ if (err != null && err.statusCode == 400)
+ error = { message: 'container `' + name + '` not found' };
+
+ callback(error, data);
+ });
+};
+
+Service.prototype.getContainerAcl = function(name, callback){
+ this.coreService.execute('get', name, {restype: 'container', comp: 'acl'}, {'x-ms-version': '2009-09-19'}, null, function(err, response) {
+ var error = null, data = null
+
+ if (err != null)
+ error = { message: err.statusCode };
+ else
+ if (response.headers && response.headers['x-ms-blob-public-access'])
+ data = response.headers['x-ms-blob-public-access'];
+
+ callback(error, data);
+ });
+};
+
+Service.prototype.setContainerAcl = function(name, level, callback){
+ var headers = {'x-ms-version': '2009-09-19'}
+ if (level) headers.merge({'x-ms-blob-public-access': level});
+ var payload = '<?xml version="1.0" encoding="utf-8"?><SignedIdentifiers />'
+
+ this.coreService.execute('put', name, {restype: 'container', comp: 'acl'}, headers, payload, function(err, response) {
+ var error = null, data = null
+
+ if (err != null)
+ error = { message: err.statusCode };
+
+ callback(error, data);
+ });
+};
+
+Service.prototype.getBlobProperties = function(name, callback){
+ this.coreService.execute('head', name, null, {'x-ms-version': '2009-09-19'}, null, function(err, response) {
+ var error = null, data = null
+
+ if (err != null && err.statusCode == 400)
+ error = { message: 'blob `' + name + '` not found' };
+ else
+ data = response.headers;
+
+ callback(error, data);
+ });
+};
+
+Service.prototype.putBlob = function(path, payload, contentType, metadata, callback){
+ contentType = contentType || "application/octet-stream";
+ headers = {'Content-Type': contentType, 'x-ms-blob-type': 'BlockBlob', 'x-ms-version': '2009-09-19', 'x-ms-blob-content-type': contentType}.merge(metadata);
+
+ this.coreService.execute('put', path, null, headers, payload, function(err, response) {
+ var error = null, data = null
+
+ if (err != null && err.statusCode == 400)
+ error = { message: 'blob `' + name + '` not found' };
+ else
+ data = response.headers;
+
+ callback(error, data);
+ });
+}
+
+Service.prototype.generateRequestUri = function(path, options){
+ return this.coreService.generateRequestUri(path, options);
+}
12 lib/waz-storage/base.js
@@ -0,0 +1,12 @@
+var Base = module.exports = exports = function Base(options) {
+ if (!options.accountName) throw new Error('accountName required');
+ if (!options.accountKey) throw new Error('accountKey required');
+ if (!options.useSsl) options.useSsl = false;
+ this.defaultConnection = options;
+
+ exports.blobs = require('../waz-blobs').load(this);
+}
+
+exports.establishConnection = function(options, callback){
+ return new Base(options);
+};
158 lib/waz-storage/core-service.js
@@ -0,0 +1,158 @@
+var crypto = require('crypto')
+ ,querystring = require('querystring')
+ ,http = require('http')
+ ,https = require('https')
+ ,url = require('url')
+ ,utils = require('./utils');
+
+exports.CoreService = new CoreService({});
+
+exports = module.exports = CoreService;
+
+function CoreService(options) {
+ this.useSasAuthOnly = options.useSasAuthOnly || false;
+ this.sharedaccesssignature = options.sharedAccessSignature;
+ this.accountName = options.accountName;
+ this.accountKey = options.accountKey;
+ this.typeOfService = options.typeOfService || "blob";
+ this.useSsl = options.useSsl || false;
+ this.useDevEnv = options.useDevEnv;
+ this.baseUrl = options.baseUrl || "core.windows.net";
+
+ if (!options.useDevEnv)
+ this.baseUrl = this.typeOfService + "." + this.baseUrl
+}
+
+CoreService.prototype.generateRequestUri = function(path, options) {
+ var protocol = this.useSsl ? "https" : "http";
+ path = path ? path = "/" + path : "/";
+
+ var params = "";
+
+ if (options && Object.keys(options).length > 0) {
+ params = "?" + Object.keys(options)
+ .filter(function(p) { return p != 'typeOfService'} )
+ .sort(function(a,b){return a.toLowerCase()>b.toLowerCase();})
+ .map(function(k){ return (k + '=' + escape(options[k])).toString(); } )
+ .join("&");
+ }
+
+ return protocol + ":\/\/" + this.accountName + '.' + this.baseUrl + path.replace("//", "/") + params;
+};
+
+CoreService.prototype.canonicalizeHeaders = function(headers){
+ return Object.keys(headers)
+ .filter(function(k) { return k.match(/^x-ms/) })
+ .sort(function(a,b){return a>b;})
+ .map(function(k){
+ return k.toLowerCase() + ":" + headers[k].toString().trim()
+ }).sort(function(a,b){return a>b;}).join("\x0A");
+};
+
+CoreService.prototype.canonicalizeMessage = function(url){
+ var uriComponent = url.replace(/https?:\/\/[^\/]+\//i, '').replace(/\?.*/i, '');
+ var compMatches = url.match(/comp=[^&]+/i);
+
+ if (compMatches)
+ uriComponent += "?" + compMatches[0]
+
+ return "/" + this.accountName + "/" + uriComponent;
+};
+
+CoreService.prototype.canonicalizeMessage20090919 = function(url){
+ var queryComponent = "";
+ var uriComponent = url.replace(/https?:\/\/[^\/]+\//i, '').replace(/\?.*/i, '');
+ var queryMatches = url.match(/\?(.*)/i);
+
+ if (queryMatches && queryMatches.length > 0) {
+ queryComponent = "\n" + queryMatches[1].split('&')
+ .map(function(p) { return unescape(p.split('=').join(':')); } )
+ .sort(function(a,b){ return a.toLowerCase()>b.toLowerCase(); })
+ .join('\n');
+ }
+
+ return "/" + this.accountName + "/" + uriComponent + queryComponent;
+}
+
+CoreService.prototype.generateSignature = function(options){
+ if (options.headers['x-ms-version'] == "2009-09-19")
+ return this.generateSignature20090919(options);
+
+ var signature = options.method.toUpperCase() + "\x0A" +
+ (options.headers["Content-MD5"] ? options.headers["Content-MD5"] : "") + "\x0A" +
+ (options.headers["Content-Type"] ? options.headers["Content-Type"] : "") + "\x0A" +
+ (options.headers["Date"] ? options.headers["Date"] : "") + "\x0A";
+
+ if (this.typeOfService != 'table')
+ signature += this.canonicalizeHeaders(options.headers) + "\x0A";
+
+ signature += this.canonicalizeMessage(options.url);
+
+ return crypto.createHmac('RSA-SHA256', this.accountKey.base64decode()).update(signature).digest('base64');
+}
+
+CoreService.prototype.generateSignature20090919 = function(options){
+ var signature = options.method.toUpperCase() + "\x0A" +
+ (options.headers["Content-Encoding"] ? options.headers["Content-Encoding"] : "") + "\x0A" +
+ (options.headers["Content-Language"] ? options.headers["Content-Language"] : "") + "\x0A" +
+ (options.headers["Content-Length"] ? options.headers["Content-Length"] : "0") + "\x0A" +
+ (options.headers["Content-MD5"] ? options.headers["Content-MD5"] : "") + "\x0A" +
+ (options.headers["Content-Type"] ? options.headers["Content-Type"] : "") + "\x0A" +
+ (options.headers["Date"] ? options.headers["Date"] : "") + "\x0A" +
+ (options.headers["If-Modified-Since"] ? options.headers["If-Modified-Since"] : "") + "\x0A" +
+ (options.headers["If-Match"] ? options.headers["If-Match"] : "") + "\x0A" +
+ (options.headers["If-None-Match"] ? options.headers["If-None-Match"] : "") + "\x0A" +
+ (options.headers["If-Unmodified-Since"] ? options.headers["If-Unmodified-Since"] : "") + "\x0A" +
+ (options.headers["Range"] ? options.headers["Range"] : "") + "\x0A" +
+ this.canonicalizeHeaders(options.headers) + "\x0A" +
+ this.canonicalizeMessage20090919(options.url);
+
+ return crypto.createHmac('RSA-SHA256', this.accountKey.base64decode()).update(signature).digest('base64');
+}
+
+CoreService.prototype.execute = function(verb, path, query, headers, payload, callback){
+ var error, data;
+ var parsedUrl = url.parse(this.generateRequestUri(path, query));
+ var headers = headers ? headers : {};
+
+ headers = headers.merge({ 'x-ms-date' : new Date().toUTCString() });
+
+ if (payload)
+ headers = headers.merge({'Content-Length' : payload.length });
+ else
+ headers = headers.merge({'Content-Length' : 0 });
+
+ var params = { method: verb, headers : headers, url : parsedUrl.href };
+
+ headers = headers.merge({ 'Authorization' : 'SharedKey ' + this.accountName + ':' + this.generateSignature(params) });
+
+ var options = {
+ host: parsedUrl.host,
+ port: parsedUrl.protocol == 'https:' ? 443: 80,
+ path: (parsedUrl.pathname + "?" + (parsedUrl.query || "")).replace(/\?$/, ''),
+ method: verb,
+ headers: headers
+ };
+
+ var request = require(parsedUrl.protocol.replace(':','')).request(options, function(response) {
+ response.setEncoding('utf8');
+
+ var body = [];
+ response.addListener('data', function (chunk) {
+ body.push(chunk);
+ });
+
+ response.addListener('end', function () {
+ if (response.statusCode >= 400)
+ error = { statusCode: response.statusCode };
+ else
+ data = { headers: response.headers, body: body.join(""), statusCode: response.statusCode };
+
+ callback(error, data);
+ });
+ });
+
+ if (payload) request.write(new Buffer(payload, 'utf-8'))
+
+ request.end();
+}
13 lib/waz-storage/index.js
@@ -0,0 +1,13 @@
+/**
+ * Base is the main export.
+ */
+
+exports = module.exports = require('./base');
+
+/**
+ * Library version.
+ *
+ * @type String
+ */
+
+exports.version = '0.0.1';
37 lib/waz-storage/utils.js
@@ -0,0 +1,37 @@
+var nibbler = require('../../support/nibbler');
+
+Object.prototype.merge = function(obj) {
+ if (!obj) obj = {};
+ var keys = Object.keys(obj);
+ for (var i = 0, len = keys.length; i < len; ++i) {
+ var key = keys[i];
+ this[key] = obj[key]
+ }
+ return this;
+};
+
+Object.prototype.isString = function() {
+ return typeof this === "string" || this instanceof String;
+};
+
+Object.prototype.flatten = function flatten() {
+ var result = [], i, len = this && this.length;
+
+ if(len && !this.isString()) {
+ for(i = 0; i < len; i++) {
+ result = result.concat(this[i].flatten());
+ }
+ } else if(len !== 0) {
+ result.push(this);
+ }
+
+ return result;
+};
+
+String.prototype.base64encode = function() {
+ return nibbler.b64encode(this);
+};
+
+String.prototype.base64decode = function() {
+ return nibbler.b64decode(this);
+};
24 package.json
@@ -0,0 +1,24 @@
+{
+ "name": "waz-storage-js",
+ "version": "0.0.1",
+ "description": "Windows Azure Storage library for Node JS",
+ "keywords": ["azure", "waz", "windows", "blobs", "queues", "tables"],
+ "homepage": "https://github.com/jpgarcia/waz-storage-js",
+ "author": "Juan Pablo Garcia <juanpablogarcia@gmail.com>",
+ "licenses": [ { "type": "MIT", "url": "http://github.com/jpgarcia/waz-storage-js/raw/master/LICENSE" } ],
+ "main": "./index.js",
+ "engines": { "node": ">= 0.2.0" },
+ "dependencies": {
+ "querystring": ">= 0.0.1",
+ "xml2js": ">= 0.1.9"
+ },
+ "devDependencies": {
+ "expresso": >= 0.8.1",
+ "assert": ">= 1.2.0",
+ "sinon": ">= 1.1.1",
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/jpgarcia/waz-tables-js.git"
+ }
+}
249 support/nibbler.js
@@ -0,0 +1,249 @@
+/*
+Adapted for Node.js by Matt Robenolt
+
+Reference: http://www.tumuski.com/2010/04/nibbler/
+*/
+
+/**
+ * Node.js example:
+ *
+ * var nibbler = require('nibbler');
+ *
+ * nibbler.b32encode('Hello, World!'); // returns JBSWY3DPFQQFO33SNRSCC====='
+ * nibbler.b32decode('JBSWY3DPFQQFO33SNRSCC====='); // returns 'Hello, World!'
+ * nibbler.b64encode('Hello, World!'); // returns 'SGVsbG8sIFdvcmxkIQ=='
+ * nibbler.b64decode('SGVsbG8sIFdvcmxkIQ=='); // returns 'Hello, World!'
+ */
+
+/*
+Copyright (c) 2010 Thomas Peri
+http://www.tumuski.com/
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+/*jslint white: true, browser: true, onevar: true, undef: true, nomen: true,
+ eqeqeq: true, plusplus: true, regexp: true, newcap: true, immed: true */
+// (good parts minus bitwise and strict, plus white.)
+
+/**
+ * Nibbler - Multi-Base Encoder
+ *
+ * version 2010-04-07
+ *
+ * Options:
+ * dataBits: The number of bits in each character of unencoded data.
+ * codeBits: The number of bits in each character of encoded data.
+ * keyString: The characters that correspond to each value when encoded.
+ * pad (optional): The character to pad the end of encoded output.
+ * arrayData (optional): If truthy, unencoded data is an array instead of a string.
+ *
+ * Example:
+ *
+ * var base64_8bit = new Nibbler({
+ * dataBits: 8,
+ * codeBits: 6,
+ * keyString: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
+ * pad: '='
+ * });
+ * base64_8bit.encode("Hello, World!"); // returns "SGVsbG8sIFdvcmxkIQ=="
+ * base64_8bit.decode("SGVsbG8sIFdvcmxkIQ=="); // returns "Hello, World!"
+ *
+ * var base64_7bit = new Nibbler({
+ * dataBits: 7,
+ * codeBits: 6,
+ * keyString: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
+ * pad: '='
+ * });
+ * base64_7bit.encode("Hello, World!"); // returns "kZdmzesQV9/LZkQg=="
+ * base64_7bit.decode("kZdmzesQV9/LZkQg=="); // returns "Hello, World!"
+ *
+ */
+var Nibbler = function (options) {
+ var construct,
+
+ // options
+ pad, dataBits, codeBits, keyString, arrayData,
+
+ // private instance variables
+ mask, group, max,
+
+ // private methods
+ gcd, translate,
+
+ // public methods
+ encode, decode;
+
+ // pseudo-constructor
+ construct = function () {
+ var i, mag, prev;
+
+ // options
+ pad = options.pad || '';
+ dataBits = options.dataBits;
+ codeBits = options.codeBits;
+ keyString = options.keyString;
+ arrayData = options.arrayData;
+
+ // bitmasks
+ mag = Math.max(dataBits, codeBits);
+ prev = 0;
+ mask = [];
+ for (i = 0; i < mag; i += 1) {
+ mask.push(prev);
+ prev += prev + 1;
+ }
+ max = prev;
+
+ // ouput code characters in multiples of this number
+ group = dataBits / gcd(dataBits, codeBits);
+ };
+
+ // greatest common divisor
+ gcd = function (a, b) {
+ var t;
+ while (b !== 0) {
+ t = b;
+ b = a % b;
+ a = t;
+ }
+ return a;
+ };
+
+ // the re-coder
+ translate = function (input, bitsIn, bitsOut, decoding) {
+ var i, len, chr, byteIn,
+ buffer, size, output,
+ write;
+
+ // append a byte to the output
+ write = function (n) {
+ if (!decoding) {
+ output.push(keyString.charAt(n));
+ } else if (arrayData) {
+ output.push(n);
+ } else {
+ output.push(String.fromCharCode(n));
+ }
+ };
+
+ buffer = 0;
+ size = 0;
+ output = [];
+
+ len = input.length;
+ for (i = 0; i < len; i += 1) {
+ // the new size the buffer will be after adding these bits
+ size += bitsIn;
+
+ // read a character
+ if (decoding) {
+ // decode it
+ chr = input.charAt(i);
+ byteIn = keyString.indexOf(chr);
+ if (chr === pad) {
+ break;
+ } else if (byteIn < 0) {
+ throw 'the character "' + chr + '" is not a member of ' + keyString;
+ }
+ } else {
+ if (arrayData) {
+ byteIn = input[i];
+ } else {
+ byteIn = input.charCodeAt(i);
+ }
+ if ((byteIn | max) !== max) {
+ throw byteIn + " is outside the range 0-" + max;
+ }
+ }
+
+ // shift the buffer to the left and add the new bits
+ buffer = (buffer << bitsIn) | byteIn;
+
+ // as long as there's enough in the buffer for another output...
+ while (size >= bitsOut) {
+ // the new size the buffer will be after an output
+ size -= bitsOut;
+
+ // output the part that lies to the left of that number of bits
+ // by shifting the them to the right
+ write(buffer >> size);
+
+ // remove the bits we wrote from the buffer
+ // by applying a mask with the new size
+ buffer &= mask[size];
+ }
+ }
+
+ // If we're encoding and there's input left over, pad the output.
+ // Otherwise, leave the extra bits off, 'cause they themselves are padding
+ if (!decoding && size > 0) {
+
+ // flush the buffer
+ write(buffer << (bitsOut - size));
+
+ // add padding keyString for the remainder of the group
+ len = output.length % group;
+ for (i = 0; i < len; i += 1) {
+ output.push(pad);
+ }
+ }
+
+ // string!
+ return (arrayData && decoding) ? output : output.join('');
+ };
+
+ /**
+ * Encode. Input and output are strings.
+ */
+ encode = function (input) {
+ return translate(input, dataBits, codeBits, false);
+ };
+
+ /**
+ * Decode. Input and output are strings.
+ */
+ decode = function (input) {
+ return translate(input, codeBits, dataBits, true);
+ };
+
+ this.encode = encode;
+ this.decode = decode;
+ construct();
+};
+
+var Base32 = new Nibbler({
+ dataBits: 8,
+ codeBits: 5,
+ keyString: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
+ pad: '='
+});
+var Base64 = new Nibbler({
+ dataBits: 8,
+ codeBits: 6,
+ keyString: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
+ pad: '='
+});
+
+exports.Nibbler = Nibbler;
+exports.b32encode = Base32.encode;
+exports.b32decode = Base32.decode;
+exports.b64encode = Base64.encode;
+exports.b64decode = Base64.decode;
22 test/waz-blobs/blob.test.js
@@ -0,0 +1,22 @@
+var waz = require('waz-storage')
+ , assert = require('assert')
+ , sinon = require('sinon')
+ , Blob = require('../../lib/waz-blobs/blob');
+
+module.exports = {
+
+ 'should return blob path from url': function(){
+ var blob = new Blob({name: 'blob_name', url: 'http://localhost/container/blob', contentType: 'application/xml'})
+ assert.equal(blob.path, "container/blob");
+ },
+
+ 'blob path should include snapshot parameter': function(){
+ var blob = new Blob({name: 'blob_name', url: 'http://localhost/container/blob?snapshot=foo', contentType: 'application/xml'})
+ assert.equal(blob.path, "container/blob?snapshot=foo");
+ },
+
+ 'blob path should not include additional parameters': function(){
+ var blob = new Blob({name: 'blob_name', url: 'http://localhost/container/blob?snapshot=foo&additional=true', contentType: 'application/xml'})
+ assert.equal(blob.path, "container/blob?snapshot=foo");
+ },
+}
314 test/waz-blobs/container.test.js
@@ -0,0 +1,314 @@
+var waz = require('waz-storage')
+ , assert = require('assert')
+ , sinon = require('sinon');
+
+module.exports = {
+
+ 'should be able to create a container': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+ var mockData = { name:"containerName" };
+
+ mock.expects("createContainer").withArgs("myContainer").yields(null, mockData).once();
+
+ waz.blobs.container.create('myContainer', function(err, container){
+ mock.verify();
+ assert.equal(container.name, "containerName");
+ assert.isNull(err);
+ });
+ },
+
+ 'should be able to delete a container': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ mock.expects("deleteContainer").withArgs("existing").yields(null, null).once();
+
+ waz.blobs.container.delete('existing', function(err){
+ mock.verify();
+ assert.isNull(err);
+ });
+ },
+
+ 'should list containers': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+ var mockData = [{ name:"container1", url:"url1", lastModified: 'today1'}, {name:"container2", url:"url2", lastModified: 'today2' }];
+
+ mock.expects("listContainers").withArgs().yields(null, mockData).once();
+
+ waz.blobs.container.list(function(err, containers){
+ mock.verify();
+
+ assert.equal(containers.length, 2);
+ assert.equal(containers[0].name, "container1");
+ assert.equal(containers[0].url, "url1");
+ assert.equal(containers[0].lastModified, "today1");
+
+ assert.equal(containers[1].name, "container2");
+ assert.equal(containers[1].url, "url2");
+ assert.equal(containers[1].lastModified, "today2");
+
+ assert.isNull(err);
+ });
+ },
+
+ 'should be able to return metadata from a given container': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+ var mockData = { 'x-meta-Name' : "containerName", 'x-meta-CustomProperty' : "customPropertyValue" };
+
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).twice();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.metadata(function(err, metadata) {
+ mock.verify();
+ assert.equal(metadata['x-meta-Name'], "containerName");
+ assert.equal(metadata['x-meta-CustomProperty'], "customPropertyValue");
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should be able to add metadata to a container': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+ var mockData = { 'x-meta-Name' : "containerName" };
+
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ var metadata = { 'x-meta-CustomProperty' : "customPropertyValue" };
+ mock.expects("setContainerProperties").withArgs("containerName", metadata).yields(null, null).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.putProperties(metadata, function(err, metadata) {
+ mock.verify();
+ assert.isNull(metadata);
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should be able to return a container by name': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+ var mockData = { xMsMetaName:"containerName" };
+
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ waz.blobs.container.find('containerName', function(err, container){
+ mock.verify();
+ assert.equal(container.name, "containerName");
+ assert.isNull(err);
+ });
+ },
+
+ 'should get None when ACL is null': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ mock.expects("getContainerAcl").withArgs("containerName").yields(null, null).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.getAcl(function(err, data){
+ mock.verify();
+ assert.equal(data, "None");
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should be able to get the ACL': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+ var acl = 'container';
+ mock.expects("getContainerAcl").withArgs("containerName").yields(null, acl).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.getAcl(function(err, data){
+ mock.verify();
+ assert.equal(data, "container");
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should set container ACL to blob': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+ mock.expects("setContainerAcl").withArgs("containerName", "blob").yields(null, null).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.setAcl('blob', function(err, data){
+ mock.verify();
+ assert.equal(data, container);
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should list blobs in a container': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ var mockBlobs = [ { name: 'blob1', url: 'http://localhost/container/blob1', contentType: 'text/xml' },
+ { name: 'blob2', url: 'http://localhost/container/blob2', contentType: 'application/json' } ]
+
+ mock.expects("listBlobs").withArgs("containerName").yields(null, mockBlobs).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.blobs(function(err, data){
+ mock.verify();
+ assert.equal(data.length, 2);
+ assert.equal(data[0].name, 'blob1');
+ assert.equal(data[0].url, 'http://localhost/container/blob1');
+ assert.equal(data[0].contentType, 'text/xml');
+
+ assert.equal(data[1].name, 'blob2');
+ assert.equal(data[1].url, 'http://localhost/container/blob2');
+ assert.equal(data[1].contentType, 'application/json');
+
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should return an empty array if container doesn\'t have blobs': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ var mockBlobs = []
+
+ mock.expects("listBlobs").withArgs("containerName").yields(null, mockBlobs).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.blobs(function(err, data){
+ mock.verify();
+ assert.equal(data.length, 0);
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should return a blob instance by a given name': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ var mockBlob = {'content-type': 'text/xml'}
+ mock.expects("getBlobProperties").withArgs("containerName/myBlob").yields(null, mockBlob).once();
+
+ var mockUrl = 'http://mock-account.blob.core.windows.net/containerName/myBlob';
+ mock.expects("generateRequestUri").withArgs("containerName/myBlob", {}).returns(mockUrl).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.getBlob('myBlob', function(err, blob){
+ mock.verify();
+ assert.equal(blob.name, 'myBlob');
+ assert.equal(blob.contentType, 'text/xml');
+ assert.equal(blob.url, 'http://mock-account.blob.core.windows.net/containerName/myBlob');
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should return a blob instance by a given un-escaped name': function(){
+ waz.establishConnection({ accountName : 'name', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ var mockBlob = {'content-type': 'text/xml'}
+ mock.expects("getBlobProperties").withArgs("containerName/my%20Blob").yields(null, mockBlob).once();
+
+ var mockUrl = 'http://mock-account.blob.core.windows.net/containerName/my%20Blob';
+ mock.expects("generateRequestUri").withArgs("containerName/my%20Blob", {}).returns(mockUrl).once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.getBlob('my Blob', function(err, blob){
+ mock.verify();
+ assert.equal(blob.name, 'my Blob');
+ assert.equal(blob.contentType, 'text/xml');
+ assert.equal(blob.url, 'http://mock-account.blob.core.windows.net/containerName/my%20Blob');
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should store a blob': function(){
+ waz.establishConnection({ accountName : 'mock-account', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ mock.expects("putBlob").withArgs("containerName/my%20Blob", '<xml><sample>value</sample></xml>', 'text/xml', {'x-ms-test': 'value'})
+ .yields(null, null)
+ .once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.store('my Blob', '<xml><sample>value</sample></xml>', 'text/xml', {'x-ms-test': 'value'}, function(err, blob){
+ mock.verify();
+ assert.equal(blob.name, 'my Blob');
+ assert.equal(blob.contentType, 'text/xml');
+ assert.equal(blob.url, 'http://mock-account.blob.core.windows.net/containerName/my%20Blob');
+ assert.isNull(err);
+ });
+ });
+ },
+
+ 'should store a blob with default content-type': function(){
+ waz.establishConnection({ accountName : 'mock-account', accountKey : 'key' });
+
+ var mock = sinon.mock(waz.blobs.container.serviceInstance);
+
+ var mockData = { 'x-meta-Name' : "containerName" };
+ mock.expects("getContainerProperties").withArgs("containerName").yields(null, mockData).once();
+
+ mock.expects("putBlob").withArgs("containerName/my%20Blob", '<xml><sample>value</sample></xml>', "application/octet-stream", {'x-ms-test': 'value'})
+ .yields(null, null)
+ .once();
+
+ waz.blobs.container.find('containerName', function (err, container) {
+ container.store('my Blob', '<xml><sample>value</sample></xml>', null, {'x-ms-test': 'value'}, function(err, blob){
+ mock.verify();
+ assert.equal(blob.name, 'my Blob');
+ assert.equal(blob.contentType, 'application/octet-stream');
+ assert.equal(blob.url, 'http://mock-account.blob.core.windows.net/containerName/my%20Blob');
+ assert.isNull(err);
+ });
+ });
+ },
+}
401 test/waz-blobs/service.test.js
@@ -0,0 +1,401 @@
+var waz = require('waz-storage')
+ , assert = require('assert')
+ , sinon = require('sinon')
+ , Service = require('../../lib/waz-blobs/service');
+
+module.exports = {
+
+ 'should create a new container': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = { body:'', headers: {'x-ms-meta-Name': 'newContainer'} };
+
+ mock.expects("execute").withArgs('put', 'newContainer', { restype: 'container' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ blobService.createContainer('newContainer', function(err, data){
+ assert.equal(data.name, "newContainer");
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should delete container': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = { body: '', headers: {'x-ms-meta-Name': 'newContainer'}, statusCode: 202 };
+
+ mock.expects("execute").withArgs('delete', 'existing', { restype: 'container' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ blobService.deleteContainer('existing', function(err, data){
+ assert.isNull(err);
+ assert.isNull(data);
+ });
+
+ mock.verify();
+ },
+
+ 'should throw when unexisting container is provided for deletion': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+
+ mock.expects("execute").withArgs('delete', 'unexisting', { restype: 'container' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields({ statusCode: 404 }, null)
+ .once();
+
+ blobService.deleteContainer('unexisting', function(err, data){
+ assert.equal(err.message, 'container `unexisting` not found');
+ assert.isNull(data);
+ });
+
+ mock.verify();
+ },
+
+ 'should report an error when creating a container that already exists': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ mock.expects("execute").withArgs('put', 'existing', { restype: 'container' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields({ statusCode: 409 }, null)
+ .once();
+
+ blobService.createContainer('existing', function(err, data){
+ assert.equal(err.message, 'container `existing` already exists');
+ assert.isNull(data);
+ });
+
+ mock.verify();
+ },
+
+ 'should get container properties': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: null};
+
+ mock.expects("execute").withArgs('get', 'mock-container', { restype: 'container' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.getContainerProperties('mock-container', function(err, properties){
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should get blob properties': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: { 'Content-Type': 'text/xml' } };
+
+ mock.expects("execute").withArgs('head', 'mock-container/blob', null, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.getBlobProperties('mock-container/blob', function(err, properties){
+ assert.equal(properties['Content-Type'], 'text/xml');
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should set container properties': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: null};
+
+ mock.expects("execute").withArgs('put', 'mock-container', { restype: 'container', comp: 'metadata' }, {'x-ms-version': '2009-09-19', 'x-ms-CustomProp': 'value'}, null)
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.setContainerProperties('mock-container', {'x-ms-CustomProp': 'value'}, function(err, properties){
+ mock.verify();
+ assert.isNull(err);
+ });
+ },
+
+ 'should get null when container ACL is not set': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: null};
+
+ mock.expects("execute").withArgs('get', 'mock-container', { restype: 'container', comp: 'acl' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.getContainerAcl('mock-container', function(err, data){
+ assert.isNull(err);
+ assert.equal(data, null)
+ });
+
+ mock.verify();
+ },
+
+ 'should get Container ACL when is set': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: {'x-ms-blob-public-access': 'blob'}};
+
+ mock.expects("execute").withArgs('get', 'mock-container', { restype: 'container', comp: 'acl' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.getContainerAcl('mock-container', function(err, data){
+ assert.isNull(err);
+ assert.equal(data, 'blob')
+ });
+
+ mock.verify();
+ },
+
+ 'should throw when error when retrieving container ACL': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+
+ mock.expects("execute").withArgs('get', 'unexisting', { restype: 'container', comp: 'acl' }, {'x-ms-version': '2009-09-19'}, null)
+ .yields({ statusCode: 400 }, null)
+ .once();
+
+ blobService.getContainerAcl('unexisting', function(err, data){
+ assert.equal(err.message, '400');
+ assert.isNull(data);
+ });
+
+ mock.verify();
+ },
+
+ 'should set container acl': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: null};
+ var payload = '<?xml version="1.0" encoding="utf-8"?><SignedIdentifiers />';
+
+ mock.expects("execute").withArgs('put', 'mock-container', { restype: 'container', comp: 'acl' }, {'x-ms-version': '2009-09-19', 'x-ms-blob-public-access': 'blob'}, payload)
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.setContainerAcl('mock-container', 'blob', function(err, properties){
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should set container acl to none': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: {}};
+ var payload = '<?xml version="1.0" encoding="utf-8"?><SignedIdentifiers />';
+
+ mock.expects("execute").withArgs('put', 'mock-container', { restype: 'container', comp: 'acl' }, {'x-ms-version': '2009-09-19'}, payload)
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.setContainerAcl('mock-container', null, function(err, properties){
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should return null when there are no containers': function(){
+
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+
+ var mockBody = '<?xml version="1.0" encoding="utf-8"?><EnumerationResults AccountName="http://jpg.blob.core.windows.net/"><Containers /><NextMarker /></EnumerationResults>';
+ var mockData = { body: mockBody, headers: null };
+
+ mock.expects("execute").withArgs('get', null, {comp: 'list'}, null, null)
+ .yields(null, mockData)
+ .once();
+
+ blobService.listContainers({}, function(err, containers){
+ assert.equal(containers.length, 0);
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should list only one container': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+
+ var mockBody = '<?xml version="1.0" encoding="utf-8"?> \
+ <EnumerationResults AccountName="http://myaccount.blob.core.windows.net"> \
+ <Containers> \
+ <Container> \
+ <Name>container1</Name> \
+ <Url>http://localhost/container1</Url> \
+ <LastModified>2009-09-11</LastModified> \
+ </Container> \
+ </Containers> \
+ </EnumerationResults>';
+
+ var mockData = { body: mockBody, headers: null };
+
+ mock.expects("execute").withArgs('get', null, {comp: 'list'}, null, null)
+ .yields(null, mockData)
+ .once();
+
+ blobService.listContainers({}, function(err, containers){
+ assert.equal(containers.length, 1);
+ assert.equal(containers[0].name, "container1");
+ assert.equal(containers[0].url, "http://localhost/container1");
+ assert.equal(containers[0].lastModified, "2009-09-11");
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should list more than one container': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+
+ var mockBody = '<?xml version="1.0" encoding="utf-8"?> \
+ <EnumerationResults AccountName="http://myaccount.blob.core.windows.net"> \
+ <Containers> \
+ <Container> \
+ <Name>container1</Name> \
+ <Url>http://localhost/container1</Url> \
+ <LastModified>2009-09-11</LastModified> \
+ </Container> \
+ <Container> \
+ <Name>container2</Name> \
+ <Url>http://localhost/container2</Url> \
+ <LastModified>2009-09-12</LastModified> \
+ </Container> \
+ </Containers> \
+ </EnumerationResults>';
+
+ var mockData = { body: mockBody, headers: null };
+ mock.expects("execute").withArgs('get', null, {comp: 'list'}, null, null)
+ .yields(null, mockData)
+ .once();
+
+ blobService.listContainers({}, function(err, containers){
+ assert.equal(containers.length, 2);
+ assert.equal(containers[0].name, "container1");
+ assert.equal(containers[0].url, "http://localhost/container1");
+ assert.equal(containers[0].lastModified, "2009-09-11");
+
+ assert.equal(containers[1].name, "container2");
+ assert.equal(containers[1].url, "http://localhost/container2");
+ assert.equal(containers[1].lastModified, "2009-09-12");
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should list blobs' : function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockBody = '<?xml version="1.0" encoding="utf-8"?> \
+ <EnumerationResults AccountName="http://myaccount.blob.core.windows.net"> \
+ <Blobs> \
+ <Blob> \
+ <Url>http://localhost/container/blob</Url> \
+ <Name>blob</Name> \
+ <Properties> \
+ <Content-Type>text/xml</Content-Type> \
+ </Properties> \
+ <Metadata> \
+ <Name>value</Name> \
+ </Metadata> \
+ </Blob> \
+ <Blob> \
+ <Url>http://localhost/container/blob2</Url> \
+ <Name>blob2</Name> \
+ <Properties> \
+ <Content-Type>application/x-stream</Content-Type> \
+ </Properties> \
+ <Metadata> \
+ <Name>value</Name> \
+ </Metadata> \
+ </Blob> \
+ </Blobs> \
+ </EnumerationResults>';
+
+ var mockData = { body: mockBody, headers: null };
+ mock.expects("execute").withArgs('get', 'myContainer', {restype: 'container', comp: 'list'}, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ blobService.listBlobs('myContainer', function(err, blobs){
+ assert.equal(blobs.length, 2);
+ assert.equal(blobs[0].name, "blob");
+ assert.equal(blobs[0].url, "http://localhost/container/blob");
+ assert.equal(blobs[0].contentType, "text/xml");
+
+ assert.equal(blobs[1].name, "blob2");
+ assert.equal(blobs[1].url, "http://localhost/container/blob2");
+ assert.equal(blobs[1].contentType, "application/x-stream");
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should return [] if any blobs found' : function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockBody = '<?xml version="1.0" encoding="utf-8"?> \
+ <EnumerationResults ContainerName="http://jpg.blob.core.windows.net/test1"> \
+ <MaxResults>1000</MaxResults> \
+ <Delimiter>/</Delimiter> \
+ <Blobs /> \
+ <NextMarker /> \
+ </EnumerationResults>';
+
+ var mockData = { body: mockBody, headers: null };
+ mock.expects("execute").withArgs('get', 'myContainer', {restype: 'container', comp: 'list'}, {'x-ms-version': '2009-09-19'}, null)
+ .yields(null, mockData)
+ .once();
+
+ blobService.listBlobs('myContainer', function(err, blobs){
+ assert.equal(blobs.length, 0);
+ assert.isNull(err);
+ });
+
+ mock.verify();
+ },
+
+ 'should store a blob': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: null};
+ var expectedHeaders = {'Content-Type': 'text/xml', 'x-ms-blob-type': 'BlockBlob', 'x-ms-version': '2009-09-19', 'x-ms-blob-content-type': 'text/xml' , 'x-ms-CustomProp': 'value'};
+
+ mock.expects("execute").withArgs('put', 'mock-container/blob', null, expectedHeaders, 'payload')
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.putBlob('mock-container/blob', 'payload', 'text/xml', {'x-ms-CustomProp': 'value'}, function(err, properties){
+ mock.verify();
+ assert.isNull(err);
+ });
+ },
+
+ 'should store a blob if metadata if not specified': function(){
+ var blobService = new Service({});
+ var mock = sinon.mock(blobService.coreService);
+ var mockData = {body: '', headers: null};
+ var expectedHeaders = {'Content-Type': 'text/xml', 'x-ms-blob-type': 'BlockBlob', 'x-ms-version': '2009-09-19','x-ms-blob-content-type': 'text/xml'};
+
+ mock.expects("execute").withArgs('put', 'mock-container/blob', null, expectedHeaders, 'payload')
+ .yields(null, mockData)
+ .once();
+
+ var properties = blobService.putBlob('mock-container/blob', 'payload', 'text/xml', null, function(err, properties){
+ mock.verify();
+ assert.isNull(err);
+ });
+ },
+}
41 test/waz-storage/base.test.js
@@ -0,0 +1,41 @@
+var waz = require('waz-storage')
+ , assert = require('assert');
+
+module.exports = {
+
+ 'should return a valid version': function(){
+ assert.match(waz.version, /^\d+\.\d+\.\d+$/);
+ },
+
+ 'should set connection properties': function(){
+ var storage = waz.establishConnection( { accountName : 'name', accountKey : 'key', useSsl : true } );
+ assert.equal(storage.defaultConnection.accountName, 'name');
+ assert.equal(storage.defaultConnection.accountKey, 'key');
+ assert.equal(storage.defaultConnection.useSsl, true);
+ },
+
+ 'should set useSsl as false by default if not provided': function(){
+ var storage = waz.establishConnection( { accountName : 'name', accountKey : 'key' } );
+ assert.equal(storage.defaultConnection.accountName, 'name');
+ assert.equal(storage.defaultConnection.accountKey, 'key');
+ assert.equal(storage.defaultConnection.useSsl, false);
+ },
+
+ 'should throw when accountName is not provided': function(){
+ try {
+ var storage = waz.establishConnection({ accountKey : 'key', useSsl : true });
+ }
+ catch(e) {
+ assert.equal(e.message, 'accountName required');
+ }
+ },
+
+ 'should throw when accountKey is not provided': function(){
+ try {
+ var storage = waz.establishConnection({accountName : 'name', useSsl : true});
+ }
+ catch(e) {
+ assert.equal(e.message, 'accountKey required');
+ }
+ }
+};
124 test/waz-storage/core-service.test.js
@@ -0,0 +1,124 @@
+var CoreService = require('waz-storage/core-service')
+ , assert = require('assert')
+ , sinon = require('sinon');
+
+module.exports = {
+
+ 'should generate URI with given operation': function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.generateRequestUri(null, { comp: 'list'}), "https://mock-account.queue.localhost/?comp=list");
+ },
+
+ 'should generate an URI without operation when operation is not given': function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.generateRequestUri("queue"), "https://mock-account.queue.localhost/queue");
+ },
+
+ 'should generate a safe URI when path includes forward slash': function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.generateRequestUri("/queue"), "https://mock-account.queue.localhost/queue");
+ },
+
+ 'should include additional parameters when given': function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.generateRequestUri("/queue", { comp: 'list', prefix: 'p'}), "https://mock-account.queue.localhost/queue?comp=list&prefix=p");
+ },
+
+ 'should include additional parameters when given althought when there is no comp': function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.generateRequestUri("/queue", { prefix: 'p', other: 'other'}), "https://mock-account.queue.localhost/queue?other=other&prefix=p");
+ },
+
+ 'should include additional parameters when given number parameters': function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.generateRequestUri("/queue", { comp: 'metadata', messagettl: 650}), "https://mock-account.queue.localhost/queue?comp=metadata&messagettl=650");
+ },
+
+ 'should escape parameter values' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.generateRequestUri("/queue", { item: '%' }), "https://mock-account.queue.localhost/queue?item=%25");
+ },
+
+ 'should canonicalize headers (order lexicographical, trim values, and join by NEW_LINES)' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ headers = { "Content-Type" : "application/xml",
+ "x-ms-prop-z": "p",
+ "x-ms-meta-name" : "a ",
+ "x-other" : "other"}
+ assert.equal(service.canonicalizeHeaders(headers), "x-ms-meta-name:a\nx-ms-prop-z:p");
+ },
+
+ 'should return empty string when no MS headers' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ headers = { "Content-Type" : "application/xml",
+ "x-other" : "other"}
+ assert.equal(service.canonicalizeHeaders(headers), "");
+ },
+
+ 'should cannonicalize message by appending account_name to the request path' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.canonicalizeMessage("http://localhost/queue?comp=list"), "/mock-account/queue?comp=list");
+ },
+
+ 'should ignore every other querystring parameter rather than comp=' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.canonicalizeMessage("http://localhost/queue?myparam=1"), "/mock-account/queue");
+ },
+
+ 'should properly canonicalize message when no parameter associated with it' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.canonicalizeMessage("http://mock-account.queue.core.windows.net/"), "/mock-account/");
+ } ,
+
+ 'should properly canonicalize message when a parameter is associated with it' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "queue", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.canonicalizeMessage("http://mock-account.queue.core.windows.net/resource?comp=list"), "/mock-account/resource?comp=list");
+ },
+
+ 'should cannonicalize message by appending account_name to the request path following 2009-09-19 version of the API' : function(){
+ var options = { accountName: "mock-account", accountKey: "mock-key", useSsl: true,
+ typeOfService: "blob", baseUrl: "localhost" };
+
+ var service = new CoreService(options);
+ assert.equal(service.canonicalizeMessage20090919("http://mock-account.blob.core.windows.net/mycontainer?restype=container&comp=metadata"), "/mock-account/mycontainer\ncomp:metadata\nrestype:container");
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.