Show:
'use strict';

var assert            = require('assert');
var crypto            = require('crypto');
var debug             = require('debug')('azure:blob');
var utils             = require('./utils');
var querystring       = require('querystring');
var xml               = require('./xml-parser');
var util              = require('util');
var events            = require('events');
var agent             = require('./agent');
var auth              = require('./authorization');

/*
 * Azure storage service version
 * @const
 */
var SERVICE_VERSION = '2016-05-31';

/*
 * The maximum size, in bytes, of a block blob that can be uploaded, before it must be separated into blocks.
 * @const
 */
var MAX_SINGLE_UPLOAD_BLOCK_BLOB_SIZE_IN_BYTES = 256 * 1024 * 1024;

/*
 * The maximum size of a single block.
 * @const
 */
var MAX_BLOCK_SIZE = 4 * 1024 * 1024;

/*
 * Page blob length.
 * @const
 */
var PAGE_SIZE = 512;

/*
 * The maximum size, in bytes, of a page blob.
 * @const
 */
var MAX_PAGE_SIZE = 1 * 1024 * 1024 * 1024;

/*
 * The maximum size of an append block.
 * @const
 */
var MAX_APPEND_BLOCK_SIZE = 4 * 1024 * 1024;

/* Transient error codes (we'll retry request when encountering these codes */
var TRANSIENT_ERROR_CODES = [
  // Azure error codes we should retry on according to azure docs
  'InternalError',
  'ServerBusy'
].concat(utils.TRANSIENT_HTTP_ERROR_CODES);

/*
 * List of query-string parameter supported in lexicographical order, used for
 * construction of the canonicalized resource.
 */
var QUERY_PARAMS_SUPPORTED = [
  'comp',
  'timeout',
  'restype',
  'prefix',
  'marker',
  'maxResults',
  'include',
  'delimiter',
  'blockid',
  'blocklisttype'
].sort();

function anonymous(method, path, query, headers) {
  // Serialize query-string
  var qs = querystring.stringify(query);
  if (qs.length > 0) {
    qs += '&';
  }
  return Promise.resolve({
    host:       this.hostname,
    method:     method,
    path:       path + '?' + qs,
    headers:    headers,
    agent:      this.options.agent
  });
}

/**
 * Blob client class for interacting with Azure Blob Storage.
 *
 * @class Blob
 * @constructor
 * @param {object} options - options on the form:
 *
 * ```js
 * {
 *   // Value for the x-ms-version header fixing the API version
 *   version:              SERVICE_VERSION,
 *
 *   // Value for the x-ms-client-request-id header identifying the client
 *   clientId:             'fast-azure-storage',
 *
 *   // Server-side request timeout
 *   timeout:              30 * 1000,
 *
 *   // Delay between client- and server-side timeout
 *   clientTimeoutDelay:   500,
 *
 *   // Max number of request retries
 *   retries:              5,
 *
 *    // HTTP Agent to use (defaults to a global azure.Agent instance)
 *   agent:                azure.Agent.globalAgent,
 *
 *   // Multiplier for computation of retry delay: 2 ^ retry * delayFactor
 *   delayFactor:          100,
 *
 *   // Randomization factor added as:
 *   // delay = delay * random([1 - randomizationFactor; 1 + randomizationFactor])
 *   randomizationFactor:  0.25,
 *
 *   // Maximum retry delay in ms (defaults to 30 seconds)
 *   maxDelay:             30 * 1000,
 *
 *   // Error codes for which we should retry
 *   transientErrorCodes:  TRANSIENT_ERROR_CODES,
 *
 *   // Azure storage accountId (required)
 *   accountId:            undefined,
 *
 *   // Azure shared accessKey, required unless options.sas is given
 *   accessKey:            undefined,
 *
 *   // Function that returns SAS string or promise for SAS string, in which
 *   // case we will refresh SAS when a request occurs less than
 *   // minSASAuthExpiry from signature expiry. This property may also be a
 *   // SAS string.
 *   sas:                  undefined,
 *
 *   // Minimum SAS expiry before refreshing SAS credentials, if a function for
 *   // refreshing SAS credentials is given as options.sas
 *   minSASAuthExpiry:     15 * 60 * 1000
 * }
 * ```
 */
function Blob(options) {
  // Initialize EventEmitter parent class
  events.EventEmitter.call(this);
  // Set default options
  this.options = {
    version:              SERVICE_VERSION,
    clientId:             'fast-azure-storage',
    timeout:              30 * 1000,
    clientTimeoutDelay:   500,
    agent:                agent.globalAgent,
    retries:              5,
    delayFactor:          100,
    randomizationFactor:  0.25,
    maxDelay:             30 * 1000,
    transientErrorCodes:  TRANSIENT_ERROR_CODES,
    accountId:            undefined,
    accessKey:            undefined,
    sas:                  undefined,
    minSASAuthExpiry:     15 * 60 * 1000,
  };

  // Overwrite default options
  for (var key in options) {
    if (options.hasOwnProperty(key) && options[key] !== undefined) {
      this.options[key] = options[key];
    }
  }

  // Validate options
  assert(this.options.accountId, "`options.accountId` must be given");

  // Construct hostname
  this.hostname = this.options.accountId + '.blob.core.windows.net';

  // Compute `timeout` for client-side timeout (in ms), and `timeoutInSeconds`
  // for server-side timeout in seconds.
  this.timeout = this.options.timeout + this.options.clientTimeoutDelay;
  this.timeoutInSeconds = Math.floor(this.options.timeout / 1000);

  // Define `this.authorize`
  if (this.options.accessKey) {
    // If set authorize to use shared key signatures
    this.authorize = auth.authorizeWithSharedKey.call(this, 'blob', QUERY_PARAMS_SUPPORTED);

    // Decode accessKey
    this._accessKey = new Buffer(this.options.accessKey, 'base64');
  } else if (this.options.sas instanceof Function) {
    // Set authorize to use shared-access-signatures with refresh function
    this.authorize = auth.authorizeWithRefreshSAS;
    // Set state with _nextSASRefresh = -1, we'll refresh on the first request
    this._nextSASRefresh = -1;
    this._sas = '';
  } else if (typeof(this.options.sas) === 'string') {
    // Set authorize to use shared-access-signature as hardcoded
    this.authorize = auth.authorizeWithSAS;
  } else {
    this.authorize = anonymous;
  }
};

// Export Blob
module.exports = Blob;

// Subclass EventEmitter
util.inherits(Blob, events.EventEmitter);

/**
 * Generate a SAS string on the form 'key1=va1&key2=val2&...'.
 *
 * @method sas
 * @param {string}  container - Name of the container that this SAS string applies to.
 * @param {string}  blob - Name of the blob that this SAS string applies to.
 * @param {object} options - Options for the following form:
 *```js
 * {
 *   start:               new Date(),             // Time from which signature is valid (optional)
 *   expiry:              new Date(),             // Expiration of signature (required).
 *   resourceType:        'blob|container',       // Specifies which resources are accessible via the SAS(required)
 *                                                // Possible values are: 'blob' or 'container'.
 *                                                // Specify 'blob' if the shared resource is a 'blob'.
 *                                                // This grants access to the content and metadata of the blob.
 *                                                // Specify 'container' if the shared resource is a 'container'.
 *                                                // This grants access to the content and metadata of any
 *                                                // blob in the container, and to the list of blobs in
 *                                                // the container.
 *   permissions: {                               // Set of permissions delegated (required)
 *                                                // It must be omitted if it has been specified in the associated
 *                                                // stored access policy.
 *     read:              false,                  // Read the content, properties, metadata or block list of a blob
 *                                                // or of any blob in the container if the resourceType is
 *                                                // a container.
 *     add:               false,                  // Add a block to an append blob or to any append blob if the
 *                                                // resourceType is a container.
 *     create:            false,                  // Write a new blob, snapshot a blob, or copy a blob
 *                                                // to a new blob.
 *                                                // These operations can be done to any blob in the container
 *                                                // if the resourceType is a container.
 *     write:             false,                  // Create or write content, properties, metadata, or block list.
 *                                                // Snapshot or lease the blob. Resize the blob (page blob only).
 *                                                // These operations can be done for every blob in the container
 *                                                // if the resourceType is a container.
 *     delete:            false,                  // Delete the blob or any blob in the container if the
 *                                                // resourceType is a container.
 *     list:              false,                  // List blobs in the container.
 *   },
 *   cacheControl:        '...',                  // The value of the Cache-Control response header
 *                                                // to be returned. (optional)
 *   contentDisposition:  '...',                  // The value of the Content-Disposition response header
 *                                                // to be returned. (optional)
 *   contentEncoding:     '...',                  // The value of the Content-Encoding response header
 *                                                // to be returned. (optional)
 *   contentLanguage:     '...',                  // The value of the Content-Language response header
 *                                                // to be returned. (optional)
 *   contentType:         '...',                  // The value of the Content-Type response header to
 *                                                // be returned. (optional)
 *   accessPolicy:        '...'                   // Reference to stored access policy (optional)
 *                                                // A GUID string
 * }
 * ```
 * @returns {string} Shared-Access-Signature on string form.
 *
 */
Blob.prototype.sas = function sas(container, blob, options){
  // verify the required options
  assert(options, "options is required");
  assert(options.expiry instanceof Date,
    "options.expiry must be a Date object");
  assert(options.resourceType, 'options.resourceType is required');
  assert(options.resourceType === 'blob' || options.resourceType === 'container',
    'The possible values for options.resourceType are `blob` or `container`');
  assert(options.permissions || options.accessPolicy, "options.permissions or options.accessPolicy must be specified");
  if (options.resourceType === 'container' && blob){
    throw new Error('If `options.resourceType` is container, the blob cannot be specified.');
  }

  // Check that we have credentials
  if (!this.options.accountId ||
    !this.options.accessKey) {
    throw new Error("accountId and accessKey are required for SAS creation!");
  }

  // Construct query-string with required parameters
  var query = {
    sv:   SERVICE_VERSION,
    se:   utils.dateToISOWithoutMS(options.expiry),
    sr:   options.resourceType === 'blob' ? 'b' : 'c',
    spr:  'https'
  }

  if (options.permissions){
    if (options.permissions.list && options.resourceType === 'blob') {
      throw new Error('The permission `list` is forbidden for the blob resource type.');
    }
    // Construct permissions string (in correct order)
    var permissions = '';
    if (options.permissions.read)    permissions += 'r';
    if (options.permissions.add)     permissions += 'a';
    if (options.permissions.create)  permissions += 'c';
    if (options.permissions.write)   permissions += 'w';
    if (options.permissions.delete)  permissions += 'd';
    if (options.permissions.list && options.resourceType === 'container') permissions += 'l';

    query.sp = permissions;
  }

  // Add optional parameters to query-string
  if (options.cacheControl)       query.rscc = options.cacheControl;
  if (options.contentDisposition) query.rscd = options.contentDisposition;
  if (options.contentEncoding)    query.rsce = options.contentEncoding;
  if (options.contentLanguage)    query.rscl = options.contentLanguage;
  if (options.contentType)        query.rsct = options.contentType;

  if (options.start) {
    assert(options.start instanceof Date, "if specified start must be a Date object");
    query.st = utils.dateToISOWithoutMS(options.start);
  }

  if (options.accessPolicy) {
    assert(/^[0-9a-fA-F]{1,64}$/i.test(options.accessPolicy), 'The `options.accessPolicy` is not valid.' );
    query.si = options.accessPolicy;
  }

  // Construct string-to-sign
  var canonicalizedResource = '/blob/' + this.options.accountId.toLowerCase() + '/' + container;
  if (blob){
    canonicalizedResource += '/' + blob;
  }
  var stringToSign = [
    query.sp || '',
    query.st || '',
    query.se || '',
    canonicalizedResource,
    query.si  || '',
    '', // TODO: Support signed IP addresses
    query.spr,
    query.sv,
    query.rscc || '',
    query.rscd || '',
    query.rsce || '',
    query.rscl || '',
    query.rsct || ''
  ].join('\n');

  // Compute signature
  query.sig = utils.hmacSha256(this._accessKey, stringToSign);

  // Return Shared-Access-Signature as query-string
  return querystring.stringify(query);
};

/**
 * Construct authorized request options by adding signature or
 * shared-access-signature, return promise for the request options.
 *
 * @protected
 * @method authorize
 * @param {string} method - HTTP verb in upper case, e.g. `GET`.
 * @param {string} path - Path on blob resource for storage account.
 * @param {object} query - Query-string parameters.
 * @param {object} header - Mapping from header key in lowercase to value.
 * @returns {Promise} A promise for an options object compatible with
 * `https.request`.
 */
Blob.prototype.authorize = function(method, path, query, headers) {
  throw new Error("authorize is not implemented, must be defined!");
};

/**
 * Make a signed request to `path` using `method` in upper-case and all `query`
 * parameters and `headers` keys in lower-case. The request will carry `data`
 * as payload and will be retried using the configured retry policy,
 *
 * @private
 * @method request
 * @param {string} method - HTTP verb in upper case, e.g. `GET`.
 * @param {string} path - Path on blob resource for storage account.
 * @param {object} query - Query-string parameters.
 * @param {object} header - Mapping from header key in lowercase to value.
 * @return {Promise} A promise for HTTPS response with `payload` property as
 * string containing the response payload.
 */
Blob.prototype.request = function request(method, path, query, headers, data) {
  // Set timeout, if not provided
  if (query.timeout === undefined) {
    query.timeout = this.timeoutInSeconds;
  }

  // Set date, version and client-request-id headers
  headers['x-ms-date']              = new Date().toUTCString();
  headers['x-ms-version']           = this.options.version;
  headers['x-ms-client-request-id'] = this.options.clientId;

  // Set content-length, if data is given
  if (data && data.length > 0 && !headers['content-length']) {
    headers['content-length'] = Buffer.byteLength(data, 'utf-8');
  }

  // Construct authorized request options with shared key signature or
  // shared-access-signature.
  var self = this;
  return this.authorize(method, path, query, headers).then(function(options) {
    // Retry with retry policy
    return utils.retry(function(retry) {
      debug("Request: %s %s, retry: %s", method, path, retry);

      // Construct a promise chain first handling the request, and then parsing
      // any potential error message
      return utils.request(options, data, self.timeout).then(function(res) {
        // Accept the response if it's 2xx, otherwise we construct and
        // throw an error
        if (200 <= res.statusCode && res.statusCode < 300) {
          return res;
        }

        // Parse error message
        var data = xml.parseError(res);

        var resMSHeaders = {};
        Object.keys(res.headers).forEach(h => {
          if (h.startsWith('x-ms-')) {
            resMSHeaders[h] = res.headers[h];
          }
        });

        // Construct error object
        var err = new Error(data.message);
        err.name = data.code + 'Error';
        err.code = data.code;
        err.statusCode = res.statusCode;
        err.message = data.message;
        err.retries = retry;
        err.resMSHeaders = resMSHeaders;

        debug("Error code: %s (%s) for %s %s on retry: %s",
              data.code, res.statusCode, method, path, retry);

        // Throw the constructed error
        throw err;
      });
    }, self.options);
  });
};

/**
 * Sets properties for a storage account’s Blob service endpoint
 *
 * @method setServiceProperties
 * @param {object} options - Options on the following form:
 * ```js
 * {
 *    logging: {                 // The Azure Analytics Logging settings.
 *      version: '...',          // The version of Storage Analytics to configure (required if logging specified)
 *      delete: true|false,      // Indicates whether all delete requests should be logged
 *                               // (required if logging specified)
 *      read: true|false,        // Indicates whether all read requests should be logged
 *                               // (required if logging specified)
 *      write: true|false,       // Indicates whether all write requests should be logged
 *                               // (required if logging specified)
 *      retentionPolicy: {
 *        enabled: true|false,   // Indicates whether a retention policy is enabled for the
 *                               // storage service. (required)
 *        days: '...',           // Indicates the number of days that metrics or logging data should be retained.
 *                               // Required only if a retention policy is enabled.
 *      },
 *    },
 *    hourMetrics: {             // The Azure Analytics HourMetrics settings
 *      version: '...',          // The version of Storage Analytics to configure
 *                               // (required if hourMetrics specified)
 *      enabled: true|false,     // Indicates whether metrics are enabled for the Blob service
 *                               //(required if hourMetrics specified).
 *      includeAPIs: true|false, // Indicates whether metrics should generate summary statistics for called API
 *                               // operations (Required only if metrics are enabled).
 *      retentionPolicy: {
 *        enabled: true|false,
 *        days: '...',
 *      },
 *    },
 *    minuteMetrics: {           // The Azure Analytics MinuteMetrics settings
 *      version: '...',          // The version of Storage Analytics to configure
 *                               // (required if minuteMetrics specified)
 *      enabled: true|false,     // Indicates whether metrics are enabled for the Blob service
 *                               // (required if minuteMetrics specified).
 *      includeAPIs: true|false, // Indicates whether metrics should generate summary statistics for called API
 *                               // operations (Required only if metrics are enabled).
 *      retentionPolicy: {
 *        enabled: true|false,
 *        days: '...',
 *      },
 *    },
 *    corsRules: [{              // CORS rules
 *      allowedOrigins: [],      // A list of origin domains that will be allowed via CORS,
 *                               // or "*" to allow all domains
 *      allowedMethods: [],      // List of HTTP methods that are allowed to be executed by the origin
 *      maxAgeInSeconds: [],     // The number of seconds that the client/browser should cache a
 *                               // preflight response
 *      exposedHeaders: [],      // List of response headers to expose to CORS clients
 *      allowedHeaders: [],      // List of headers allowed to be part of the cross-origin request
 *    }]
 * }
 * ```
 * @return {Promise} A promise that the properties have been set
 */
Blob.prototype.setServiceProperties = function setServiceProperties(options) {

  var payload = '<?xml version="1.0" encoding="utf-8"?>';
  payload += '<StorageServiceProperties>';

  if (options) {
    if (options.logging) {
      payload += '<Logging>';
      var logging = options.logging;
      assert(logging.version, 'The `options.logging.version` must be supplied if `options.logging` is specified');
      payload += '<Version>' + logging.version + '</Version>';

      assert(logging.delete, 'The `options.logging.delete` must be supplied if `options.logging` is specified');
      payload += '<Delete>' + logging.delete + '</Delete>';

      assert(logging.read, 'The `options.logging.read` must be supplied if `options.logging` is specified');
      payload += '<Read>' + logging.read + '</Read>';

      assert(logging.write, 'The `options.logging.write` must be supplied if `options.logging` is specified');
      payload += '<Write>' + logging.write + '</Write>';

      assert(logging.retentionPolicy, 'The `options.logging.retentionPolicy` must be supplied if `options.logging` is specified');
      payload += '<RetentionPolicy>';
      assert(logging.retentionPolicy.enabled, 'The `options.logging.retentionPolicy.enabled` must be supplied if `options.logging` is specified');
      payload += '<Enabled>' + logging.retentionPolicy.enabled + '</Enabled>';
      if (logging.retentionPolicy.enabled === true) {
        assert(logging.retentionPolicy.days, 'The `options.logging.retentionPolicy.days` must be supplied if a retention policy is enabled');
        assert(logging.retentionPolicy.days > 1 && logging.retentionPolicy.days < 365,
          'The `options.logging.retentionPolicy.days` must be a number between 1 and 365.');
        payload += '<Days>' + logging.retentionPolicy.days + '</Days>';
      }
      payload += '</RetentionPolicy>';

      payload += '</Logging>';
    }

    if(options.hourMetrics) {
      payload += '<HourMetrics>';
      var hourMetrics = options.hourMetrics;

      assert(hourMetrics.version, 'The `options.hourMetrics.version` must be supplied if `options.hourMetrics` is specified');
      payload += '<Version>' + hourMetrics.version + '</Version>';

      if (hourMetrics.enabled === undefined || hourMetrics.enabled === null) {
        throw new Error('The `options.hourMetrics.enabled` must be supplied if `options.hourMetrics` is specified');
      }
      payload += '<Enabled>' + hourMetrics.enabled + '</Enabled>';

      if (hourMetrics.enabled === true) {
        if (hourMetrics.includeAPIs === undefined || hourMetrics.includeAPIs === null) {
          throw new Error('The `options.hourMetrics.includeAPIs` must be supplied if `options.hourMetrics` is specified');
        }
        payload += '<IncludeAPIs>' + hourMetrics.includeAPIs + '</IncludeAPIs>';
      }

      assert(hourMetrics.retentionPolicy, 'The `options.hourMetrics.retentionPolicy` must be supplied if `options.hourMetrics` is specified');
      payload += '<RetentionPolicy>';
      if (hourMetrics.retentionPolicy.enabled === undefined || hourMetrics.retentionPolicy.enabled === null) {
        throw new Error('The `options.hourMetrics.retentionPolicy.enabled` must be supplied if `options.hourMetrics` is specified');
      }
      payload += '<Enabled>' + hourMetrics.retentionPolicy.enabled + '</Enabled>';
      if (hourMetrics.retentionPolicy.enabled === true) {
        assert(hourMetrics.retentionPolicy.days, 'The `options.hourMetrics.retentionPolicy.days` must be supplied if a retention policy is enabled');
        assert(hourMetrics.retentionPolicy.days > 1 && hourMetrics.retentionPolicy.days < 365,
          'The `options.hourMetrics.retentionPolicy.days` must be a number between 1 and 365.');
        payload += '<Days>' + hourMetrics.retentionPolicy.days + '</Days>';
      }
      payload += '</RetentionPolicy>';

      payload += '</HourMetrics>';
    }

    if(options.minuteMetrics) {
      payload += '<MinuteMetrics>';
      var minuteMetrics = options.minuteMetrics;

      assert(minuteMetrics.version, 'The `options.minuteMetrics.version` must be supplied if `options.minuteMetrics` is specified');
      payload += '<Version>' + minuteMetrics.version + '</Version>';

      if (minuteMetrics.enabled === undefined || minuteMetrics.enabled === null) {
        throw new Error('The `options.minuteMetrics.enabled` must be supplied if `options.minuteMetrics` is specified');
      }
      payload += '<Enabled>' + minuteMetrics.enabled + '</Enabled>';

      if (minuteMetrics.enabled === true) {
        if (minuteMetrics.includeAPIs === undefined || minuteMetrics.includeAPIs === null) {
          throw new Error('The `options.minuteMetrics.includeAPIs` must be supplied if `options.minuteMetrics` is specified');
        }
        payload += '<IncludeAPIs>' + minuteMetrics.includeAPIs + '</IncludeAPIs>';
      }

      assert(minuteMetrics.retentionPolicy, 'The `options.minuteMetrics.retentionPolicy` must be supplied if `options.minuteMetrics` is specified');
      payload += '<RetentionPolicy>';
      if (minuteMetrics.retentionPolicy.enabled === undefined || minuteMetrics.retentionPolicy.enabled === null) {
        throw new Error('The `options.minuteMetrics.retentionPolicy.enabled` must be supplied if `options.minuteMetrics` is specified');
      }
      payload += '<Enabled>' + minuteMetrics.retentionPolicy.enabled + '</Enabled>';
      if (minuteMetrics.retentionPolicy.enabled === true) {
        assert(minuteMetrics.retentionPolicy.days, 'The `options.minuteMetrics.retentionPolicy.days` must be supplied if a retention policy is enabled');
        assert(minuteMetrics.retentionPolicy.days >= 1 && minuteMetrics.retentionPolicy.days <= 365,
          'The `options.minuteMetrics.retentionPolicy.days` must be a number between 1 and 365.');
        payload += '<Days>' + minuteMetrics.retentionPolicy.days + '</Days>';
      }
      payload += '</RetentionPolicy>';

      payload += '</MinuteMetrics>';
    }

    if(options.corsRules) {
      payload += '<Cors>';
      options.corsRules.forEach(function(rule) {
        payload += '<CorsRule>';

        assert(rule.allowedOrigins, 'For CORS rule, the allowedOrigins must be specified');
        payload += '<AllowedOrigins>' + rule.allowedOrigins.join(',') + '</AllowedOrigins>';

        assert(rule.allowedMethods, 'For CORS rule, the allowedMethods must be specified');
        payload += '<AllowedMethods>' + rule.allowedMethods.join(',') + '</AllowedMethods>';

        assert(rule.maxAgeInSeconds, 'For CORS rule, the maxAgeInSeconds must be specified');
        if (rule.maxAgeInSeconds) payload += '<MaxAgeInSeconds>' + rule.maxAgeInSeconds + '</MaxAgeInSeconds>';

        assert(rule.exposedHeaders, 'For CORS rule, the exposedHeaders must be specified');
        if (rule.exposedHeaders) payload += '<ExposedHeaders>' + rule.exposedHeaders.join(',') + '</ExposedHeaders>';

        assert(rule.allowedHeaders, 'For CORS rule, the allowedHeaders must be specified');
        if (rule.allowedHeaders) payload += '<AllowedHeaders>' + rule.allowedHeaders.join(',') + '</AllowedHeaders>';

        payload += '</CorsRule>';
      });
      payload += '</Cors>';
    }
  }
  payload += '</StorageServiceProperties>';

  var query = {
    restype: 'service',
    comp: 'properties'
  };

  return this.request('PUT', '/', query, {}, payload).then(function(response) {
    if (response.statusCode !== 202) {
      throw new Error("setServiceProperties: Unexpected statusCode: " + response.statusCode);
    }
  });
};

/**
 * Gets the properties of a storage account’s Blob service, including properties for Storage Analytics and
 * CORS (Cross-Origin Resource Sharing) rules.
 *
 * @method getServiceProperties
 * @return {Promise} A promise for an object on the form:
 * ```js
 * {
 *    logging: {                  // The Azure Analytics Logging settings.
 *      version: '...',           // The version of Storage Analytics to configure
 *      delete: true|false,       // Indicates whether all delete requests should be logged
 *      read: true|false,         // Indicates whether all read requests should be logged
 *      write: true|false,        // Indicates whether all write requests should be logged
 *      retentionPolicy: {
 *        enabled: true|false,    // Indicates whether a retention policy is enabled for the storage service
 *        days: '...',            // Indicates the number of days that metrics or logging data should be retained.
 *      },
 *    },
 *    hourMetrics: {              // The Azure Analytics HourMetrics settings
 *      version: '...',           // The version of Storage Analytics to configure
 *      enabled: true|false,      // Indicates whether metrics are enabled for the Blob service
 *      includeAPIs: true|false,  // Indicates whether metrics should generate summary statistics
 *                                // for called API operations.
 *      retentionPolicy: {
 *        enabled: true|false,
 *        days: '...',
 *      },
 *    },
 *    minuteMetrics: {            // The Azure Analytics MinuteMetrics settings
 *      version: '...',           // The version of Storage Analytics to configure
 *      enabled: true|false,      // Indicates whether metrics are enabled for the Blob service
 *      includeAPIs: true|false,  // Indicates whether metrics should generate summary statistics
 *                                // for called API operations.
 *      retentionPolicy: {
 *        enabled: true|false,
 *        days: '...',
 *      },
 *    },
 *    corsRules: [{               // CORS rules
 *      allowedOrigins: [],       // A list of origin domains that will be allowed via CORS,
 *                                // or "*" to allow all domains.
 *      allowedMethods: [],       // List of HTTP methods that are allowed to be executed by the origin
 *      maxAgeInSeconds: [],      // The number of seconds that the client/browser should cache a preflight response
 *      exposedHeaders: [],       // List of response headers to expose to CORS clients
 *      allowedHeaders: [],       // List of headers allowed to be part of the cross-origin request
 *    }]
 * }
 * ```
 */
Blob.prototype.getSeviceProperties = function getSeviceProperties() {
  var query = {
    restype: 'service',
    comp: 'properties'
  };
  return this.request('GET', '/', query, {}).then(function(response) {
    if (response.statusCode !== 200) {
      throw new Error("setServiceProperties: Unexpected statusCode: " + response.statusCode);
    }

    return xml.blobParseServiceProperties(response);
  });
};

/**
 * Create a new container with the given 'name' under the storage account.
 *
 * @method createContainer
 * @param {string} name -  Name of the container to create
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    metadata: '...',          // Mapping from metadata keys to values. (optional)
 *    publicAccessLevel: '...', // Specifies whether data in the container may be accessed
 *                              // publicly and the level of access.
 *                              // Possible values: container, blob. (optional)
 * }
 * ```
 * @returns {Promise} a promise for metadata key/value pair
 * A promise for an object on the form:
 * ```js
 * {
 *      eTag: '...',               // The entity tag of the container
 *      lastModified: '...',       // The date/time the container was last modified
 * }
 * ```
 */
Blob.prototype.createContainer = function createContainer(name, options) {
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  // Construct headers
  var headers = {};
  if (options){
    if (options.metadata) {
      for(var key in options.metadata) {
        if (options.metadata.hasOwnProperty(key)) {
          headers['x-ms-meta-' + key] = options.metadata[key];
        }
      }
    }
    if(options.publicAccessLevel) {
      assert( options.publicAccessLevel === 'container' || options.publicAccessLevel === 'blob',
        'The `publicAccessLevel` is invalid. The possible values are: container and blob.'
      )
      headers['x-ms-blob-public-access'] = options.publicAccessLevel;
    }
  }

  // Construct query string
  var query = {
    restype: 'container'
  };
  var path = '/' + name;
  return this.request('PUT', path, query, headers).then(function(response) {
    // container was created - response code 201
    if (response.statusCode !== 201) {
      throw new Error("createContainer: Unexpected statusCode: " + response.statusCode);
    }

    return {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified'])
    };
  });
};

/**
 * Sets metadata for the specified container.
 * Overwrites all existing metadata that is associated with the container.
 *
 * @method setContainerMetadata
 * @param {string} name - Name of the container to set metadata on
 * @param {object} metadata - Mapping from metadata keys to values.
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    leaseId: '...',               // Lease unique identifier. A GUID string.(optional)
 *    ifModifiedSince: new Date(),  // Specify this to perform the operation only if the resource has been
 *                                  // modified since the specified time. (optional)
 * }
 *```
 * @returns {Promise} a promise for metadata key/value pair
 * A promise for an object on the form:
 * ```js
 * {
 *      eTag: '...',               // The entity tag of the container
 *      lastModified: '...',       // The date/time the container was last modified
 * }
 * ```
 */
Blob.prototype.setContainerMetadata = function setContainerMetadata(name, metadata, options) {
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  // Construct query string
  var query = {
    restype: 'container',
    comp: 'metadata'
  };
  // Construct headers
  var headers = {};
  if (options) {
    if (options.leaseId) {
      assert(utils.isValidGUID(options.leaseId), '`leaseId` is not a valid GUID.');
      headers['x-ms-lease-id'] = options.leaseId;
    }
    // set conditional header
    if (options.ifModifiedSince){
      assert(options.ifModifiedSince instanceof Date,
        'If specified, the `options.ifModifiedSince` must be a Date');
      headers['if-modified-since'] = options.ifModifiedSince.toUTCString();
    }
  }

  for(var key in metadata) {
    if (metadata.hasOwnProperty(key)) {
      headers['x-ms-meta-' + key] = metadata[key];
    }
  }
  var path = "/" + name;
  return this.request('PUT', path, query, headers).then(function(response) {
    if(response.statusCode !== 200) {
      throw new Error('setContainerMetadata: Unexpected statusCode: ' + response.statusCode);
    }
    return {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified'])
    }
  });
};

/**
 * Get the metadata for the container with the given name.
 *
 * Note, this is a `HEAD` request, so if the container is missing you get an
 * error with `err.statusCode = 404`, but `err.code` property will be
 * `ErrorWithoutCode`.
 *
 * @method getContainerMetadata
 * @param {string} name - the name of the container to get metadata from.
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    leaseId: '...'  // Lease unique identifier. A GUID string.(optional)
 * }
 * @returns {Promise} a promise for metadata key/value pair
 * A promise for an object on the form:
 * ```js
 * {
 *      eTag: '...',               // The entity tag of the container
 *      lastModified: '...',       // The date/time the container was last modified
 * }
 * ```
 */
Blob.prototype.getContainerMetadata = function getContainerMetadata(name, options) {
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  // Construct the query string
  var query = {
    comp: 'metadata',
    restype: 'container'
  }
  var path = "/" + name;
  var headers = {};
  if (options && options.leaseId) {
    assert(utils.isValidGUID(options.leaseId), '`leaseId` is not a valid GUID.');
    headers['x-ms-lease-id'] = options.leaseId;
  }

  return this.request('HEAD', path, query, headers).then(function(response) {
    if (response.statusCode !== 200) {
      throw new Error("getContainerMetadata: Unexpected statusCode: " + response.statusCode);
    }
    return {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified']),
      metadata: utils.extractMetadataFromHeaders(response)
    }
  });
};

/**
 * Delete container with the given 'name'.
 *
 * Note, when a container is deleted, a container with the same name cannot be created for at least 30 seconds;
 * the container may not be available for more than 30 seconds if the service is still processing the request.
 * Please see the documentation for more details.
 *
 * @method deleteContainer
 * @param {string} name -  Name of the container to delete
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    leaseId: '...',                   // Lease unique identifier. A GUID string.(optional)
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has
 *                                      // been modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has
 *                                      // not been modified since the specified date/time. (optional)
 * }
 *```
 * @returns {Promise} A promise that container has been marked for deletion.
 */
Blob.prototype.deleteContainer = function deleteContainer(name, options) {
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  // construct query string
  var query = {
    restype: 'container'
  };
  var path = '/' + name;
  var headers = {};
  if (options) {
    if (options.leaseId) {
      assert(utils.isValidGUID(options.leaseId), '`leaseId` is not a valid GUID.');
      headers['x-ms-lease-id'] = options.leaseId;
    }

    utils.setConditionalHeaders(headers, options, true);
  }

  return this.request('DELETE', path, query, headers).then(function(response) {
    if(response.statusCode !== 202) {
      throw new Error('deleteContainer: Unexpected statusCode: ' + response.statusCode);
    }
  });
};

/**
 * List the containers under the storage account
 *
 * @method listContainers
 * @param {object} options - Options on the following form:
 *
 * ```js
 * {
 *   prefix:          '...',    // Prefix of containers to list
 *   marker:          '...',    // Marker to list containers from
 *   maxResults:      5000,     // Max number of results
 *   metadata:        false     // Whether or not to include metadata
 * }
 *
 * @returns {Promise} A promise for an object on the form:
 * ```js
 * {
 *   containers: [
 *     {
 *       name:       '...',           // Name of container
 *       properties: {
 *          lastModified: '...',      // Container's last modified time
 *          eTag: '...',              // The entity tag of the container
 *          leaseStatus: '...',       // The lease status of the container
 *          leaseState: '...',        // The lease state of the container
 *          leaseDuration: '...'      // The lease duration of the container
 *          publicAccessLevel: '...'  // Indicates whether data in the container may be accessed publicly
 *                                    // and the level of access. If this is not returned in the response,
 *                                    // the container is private to the account owner.
 *       }
 *       metadata:   {}               // Meta-data dictionary if requested
 *     }
 *   ],
 *   prefix:         '...',           // prefix given in options (if given)
 *   marker:         '...',           // marker given in options (if given)
 *   maxResults:     5000,            // maxResults given in options (if given)
 *   nextMarker:     '...'            // Next marker if not at end of list
 * }
 * ```
 */
Blob.prototype.listContainers = function listContainers(options) {
  // Ensure options
  options = options || {};

  // Construct query string
  var query = {
    comp: 'list'
  };
  if (options.prefix)     query.prefix      = options.prefix;
  if (options.marker)     query.marker      = options.marker;
  if (options.maxResults) query.maxresults  = options.maxResults;
  if (options.metadata)   query.include     = 'metadata';

  return this.request('GET', '/', query, {}).then(function(response) {
    if(response.statusCode !== 200) {
      throw new Error('listContainers: Unexpected statusCode: ' + response.statusCode);
    }
    return xml.blobParseListContainers(response);
  });
};

/**
 * Get all user-defined metadata and system properties for the container with the given name.
 *
 * Note, this is a `HEAD` request, so if the container is missing you get an
 * error with `err.statusCode = 404`, but `err.code` property will be
 * `ErrorWithoutCode`.
 *
 * @method getContainerProperties
 * @param {string} name - The name of the container to get properties from.
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    leaseId: '...' // GUID string; lease unique identifier (optional)
 * }
 * ```
 * @returns {Promise} A promise for an object on the form:
 * ```js
 * {
 *   metadata: {                 // Mapping from meta-data keys to values
 *     '<key>':      '<value>',  // Meta-data key/value pair
 *     ...
 *   },
 *   properties: {                // System properties
 *     eTag:          '...',      // The entity tag for the container
 *     lastModified: '...'        // The date and time the container was last modified
 *     leaseStatus: '...',        // The lease status of the container
 *     leaseState:  '...',        // Lease state of the container
 *     leaseDuration: '...',      // Specifies whether the lease on a container is of infinite or fixed duration.
 *     publicAccessLevel: '...',  // Indicates whether data in the container may be accessed publicly and
 *                                // the level of access. If this is not returned in the response,
 *                                // the container is private to the account owner.
 *   }
 * }
 * ```
 */
Blob.prototype.getContainerProperties = function getContainerProperties(name, options) {
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  var query = {
    restype: 'container'
  }
  var path = '/' + name;
  var headers = {};
  if (options && options.leaseId) {
    assert(utils.isValidGUID(options.leaseId), '`leaseId` is not a valid GUID.');
    headers['x-ms-lease-id'] = options.leaseId;
  }

  return this.request('HEAD', path, query, headers).then(function(response) {
    if (response.statusCode !== 200) {
      throw new Error("getContainerProperties: Unexpected statusCode: " + response.statusCode);
    }

    // Extract metadata
    var metadata = utils.extractMetadataFromHeaders(response);

    // Extract system properties
    var properties = {};
    properties.eTag = response.headers.etag;
    properties.lastModified = new Date(response.headers['last-modified']);
    properties.leaseStatus = response.headers['x-ms-lease-status'];
    properties.leaseState = response.headers['x-ms-lease-state'];
    if (response.headers['x-ms-lease-duration']) {
      properties.leaseDuration = response.headers['x-ms-lease-duration'];
    }
    if (response.headers['x-ms-blob-public-access']) {
      properties.publicAccessLevel = response.headers['x-ms-blob-public-access'];
    }

    return {
      metadata: metadata,
      properties: properties
    }
  });
};

/**
 * Gets the permissions for the container with the given name
 *
 * @method getContainerACL
 * @param {string} name - Name of the container to get ACL from
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    leaseId: '...' // GUID string; lease unique identifier (optional)
 * }
 * ```
 * @returns {Promise} A promise for permissions
 * ```js
 * {
 *    eTag: '...',                      // The entity tag of the container
 *    lastModified: '...',              // The date/time the container was last modified
 *    publicAccessLevel: '...',         // Indicate whether blobs in a container may be accessed publicly.(optional)
 *                                      // Possible values: container (full public read access for container
 *                                      // and blob data) or blob (public read access for blobs)
 *                                      // If it is not specified, the resource will be private and will be
 *                                      // accessed only by the account owner.
 *    accessPolicies: [{                // The container ACL settings.
 *                                      // An array with five maximum access policies objects (optional)
 *      id:     '...',                  // Unique identifier, up to 64 chars in length
 *      start:  new Date(),             // Time from which access policy is valid
 *      expiry: new Date(),             // Expiration of access policy
 *      permission: {                   // Set of permissions delegated
 *        read:              false,     // Read the content, properties, metadata or block list of a blob or, of
 *                                      // any blob in the container if the resource is a container.
 *        add:               false,     // Add a block to an append blob or, to any append blob
 *                                      // if the resource is a container.
 *        create:            false,     // Write a new blob, snapshot a blob, or copy a blob to a new blob.
 *                                      // These operations can be done to any blob in the container
 *                                      // if the resource is a container.
 *        write:             false,     // Create or write content, properties, metadata, or block list.
 *                                      // Snapshot or lease the blob. Resize the blob (page blob only).
 *                                      // These operations can be done for every blob in the container
 *                                      // f the resource is a container.
 *        delete:            false,     // Delete the blob or, any blob in the container if the resource
 *                                      // is a container.
 *        list:              false,     // List blobs in the container.
 *      }
 *    }]
 * }
 * ```
 */
Blob.prototype.getContainerACL = function getContainerACL(name, options){
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  var query = {
    restype: 'container',
    comp: 'acl'
  }
  var path = '/' + name;

  var headers = {};
  if (options && options.leaseId) {
    assert(utils.isValidGUID(options.leaseId), '`leaseId` is not a valid GUID.');
    headers['x-ms-lease-id'] = options.leaseId;
  }

  return this.request('GET', path, query, headers).then(function (response) {
    if (response.statusCode !== 200) {
      throw new Error("getContainerACL: Unexpected statusCode: " + response.statusCode);
    }
    return {
      accessPolicies: xml.blobParseContainerACL(response),
      publicAccessLevel: response.headers['x-ms-blob-public-access'],
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified'])
    };
  });
};

/**
 * Sets the permissions for the container with the given name.
 * The permissions indicate whether blobs in a container may be accessed publicly.
 *
 * @method setContainerACL
 * @param {string} name - Name of the container to set ACL to
 * @param {object} options - Options on the following form
 *```js
 * {
 *    publicAccessLevel: '...',       // Indicate whether blobs in a container may be accessed publicly.(optional)
 *                                    // Possible values: container (full public read access for container
 *                                    // and blob data) or blob (public read access for blobs).
 *                                    // If it is not specified, the resource will be private and will be accessed
 *                                    // only by the account owner.
 *    accessPolicies: [{              // The container ACL settings.
 *                                    // An array with five maximum access policies objects (optional)
 *      id:     '...',                // Unique identifier, up to 64 chars in length
 *      start:  new Date(),           // Time from which access policy is valid
 *      expiry: new Date(),           // Expiration of access policy
 *      permission: {                 // Set of permissions delegated
 *        read:              false,   // Read the content, properties, metadata or block list of a blob or of
 *                                    // any blob in the container if the resourceType is a container.
 *        add:               false,   // Add a block to an append blob or to any append blob
 *                                    // if the resourceType is a container.
 *        create:            false,   // Write a new blob, snapshot a blob, or copy a blob to a new blob.
 *                                    // These operations can be done to any blob in the container
 *                                    // if the resourceType is a container.
 *        write:             false,   // Create or write content, properties, metadata, or block list.
 *                                    // Snapshot or lease the blob. Resize the blob (page blob only).
 *                                    // These operations can be done for every blob in the container
 *                                    // if the resourceType is a container.
 *        delete:            false,   // Delete the blob or any blob in the container
 *                                    // if the resourceType is a container.
 *        list:              false,   // List blobs in the container.
 *      }
 *    }],
 *    leaseId: '...',                 // GUID string; lease unique identifier (optional)
 *    ifModifiedSince: new Date(),    // Specify this to perform the operation only if the resource has
 *                                    // been modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),  // Specify this to perform the operation only if the resource has
 *                                    // not been modified since the specified date/time. (optional)
 * }
 * ```
 * @returns {Promise} a promise for metadata key/value pair
 * A promise for an object on the form:
 * ```js
 * {
 *      eTag: '...',               // The entity tag of the container
 *      lastModified: '...',       // The date/time the container was last modified
 * }
 * ```
 */
Blob.prototype.setContainerACL = function setContainerACL(name, options) {
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  var query = {
    restype: 'container',
    comp: 'acl'
  };
  var path = '/' + name;
  var headers = {};
  if (options) {
    if (options.leaseId) {
      assert(utils.isValidGUID(options.leaseId), '`leaseId` is not a valid GUID.');
      headers['x-ms-lease-id'] = options.leaseId;
    }

    if (options.publicAccessLevel){
      assert(options.publicAccessLevel === 'container' || options.publicAccessLevel === 'blob',
        "The supplied `publicAccessLevel` is incorrect. The possible values are: container and blob")
      headers['x-ms-blob-public-access'] = options.publicAccessLevel;
    }
    if (options.accessPolicies && options.accessPolicies.length > 5){
      throw new Error("The supplied access policy is wrong. The maximum number of the access policies is 5");
    }

    // Construct the payload
    var data = '<?xml version="1.0" encoding="utf-8"?>';
    data += '<SignedIdentifiers>';
    if (options.accessPolicies) {
      options.accessPolicies.forEach(function(policy){
        assert(/^[0-9a-fA-F]{1,64}$/i.test(policy.id), 'The access policy id is not valid.' );

        data += '<SignedIdentifier><Id>' + policy.id + '</Id>';
        data += '<AccessPolicy>';
        if (policy.start) {
          assert(policy.start instanceof Date, "If specified, policy.start must be a Date object");
          data += '<Start>' + utils.dateToISOWithoutMS(policy.start) + '</Start>';
        }
        if (policy.expiry) {
          assert(policy.expiry instanceof Date, "If specified, policy.expiry must be a Date object");
          data += '<Expiry>' + utils.dateToISOWithoutMS(policy.expiry) + '</Expiry>';
        }

        if (policy.permission) {
          var permissions = '';
          if (policy.permission.read)    permissions += 'r';
          if (policy.permission.add)     permissions += 'a';
          if (policy.permission.create)  permissions += 'c';
          if (policy.permission.write)   permissions += 'w';
          if (policy.permission.delete)  permissions += 'd';
          if (policy.permission.list)    permissions += 'l';

          data += '<Permission>' + permissions + '</Permission>';
        }

        data += '</AccessPolicy></SignedIdentifier>';
      });
    }

    data += '</SignedIdentifiers>';

    utils.setConditionalHeaders(headers, options, true);
  }

  return this.request('PUT', path, query, headers, data).then(function(response){
    if(response.statusCode !== 200){
      throw new Error("setContainerACL: Unexpected statusCode: " + response.statusCode);
    }

    return {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified'])
    }
  });
};

/**
 * Get the list of blobs under the specified container.
 *
 * @method listBlobs
 * @param {string} container - Name of the container(required)
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    prefix: '...',              // Prefix of blobs to list (optional)
 *    delimiter: '...',           // Delimiter, i.e. '/', for specifying folder hierarchy. (optional)
 *    marker: '...',              // Marker to list blobs from (optional)
 *    maxResults: 5000,           // The maximum number of blobs to return (optional)
 *    include: {                  // Specifies one or more datasets to include in the response (optional)
 *      snapshots: false,         // Include snapshots in listing
 *      metadata: false,          // Include blob metadata in listing
 *      uncommittedBlobs: false,  // Include uncommitted blobs in listing
 *      copy: false               // Include metadata related to any current or previous Copy Blob operation
 *    }
 * }
 * ```
 * @returns {Promise} A promise for an object on the form:
 * ```js
 * {
 *   blobs: [
 *     {
 *       name:       '...',               // Name of blob
 *       snapshot:    '...',              // A date and time value that uniquely identifies the snapshot
 *                                        // relative to its base blob
 *       properties:  {
 *          lastModified: '...',          // The date and time the blob was last modified
 *          eTag: '...',                  // The entity tag of the blob
 *          contentLength: '...',         // The content length of the blob
 *          contentType: '...',           // The MIME content type of the blob
 *          contentEncoding: '...',       // The content encoding of the blob
 *          contentLanguage: '...',       // The content language of the blob
 *          contentMD5: '...',            // An MD5 hash of the blob content
 *          cacheControl: '...',          // The blob cache control
 *          xmsBlobSequenceNumber: '...', // The page blob sequence number
 *          blobType: '...',              // The type of the blob: BlockBlob | PageBlob | AppendBlob
 *          leaseStatus: '...',           // The lease status of the blob
 *          leaseState: '...',            // The lease state of the blob
 *          leaseDuration: '...',         // The lease duration of the blob
 *          copyId: '...',                // String identifier for the copy operation
 *          copyStatus: '...',            // The state of the copy operation: pending | success | aborted | failed
 *          copySource: '...',            // The name of the source blob of the copy operation
 *          copyProgress: '...',          // The bytes copied/total bytes
 *          copyCompletionTime: '...',    // The date and time the copy operation finished
 *          copyStatusDescription: '...', // The status of the copy operation
 *          serverEncrypted: false,       // true if the blob and application metadata are completely encrypted,
 *                                        // and false otherwise
 *          incrementalCopy: '...',       // true for the incremental copy blobs operation and snapshots
 *       }
 *       metadata:   {}                   // Meta-data dictionary if requested
 *     }
 *   ],
 *   blobPrefixName: '...',
 *   prefix:         '...',               // prefix given in options (if given)
 *   marker:         '...',               // marker given in options (if given)
 *   maxResults:     5000,                // maxResults given in options (if given)
 *   nextMarker:     '...'                // Next marker if not at end of list
 *   delimiter:      '...'                // Delimiter
 * }
 * ```
 */
Blob.prototype.listBlobs = function listBlobs(container, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  // Construct the query string
  var query = {
    restype: 'container',
    comp: 'list'
  };

  assert(container, 'The container name must be specified');

  if (options) {
    if (options.prefix)     query.prefix      = options.prefix;
    if (options.marker)     query.marker      = options.marker;
    if (options.maxResults) query.maxresults  = options.maxResults;
    if (options.include)  {
      var includeValues = [];
      if (options.include.snapshot) includeValues.push('snapshot');
      if (options.include.metadata) includeValues.push('metadata');
      if (options.include.uncommittedBlobs) includeValues.push('uncommittedblobs');
      if (options.include.copy) includeValues.push('copy');

      query.include = includeValues.join(',');
    }
    if (options.delimiter)  query.delimiter = options.delimiter;
  }

  var path = '/' + container;
  var headers = {};

  return this.request('GET', path, query, headers).then(function(response){
    if(response.statusCode !== 200){
      throw new Error("listBlobs: Unexpected statusCode: " + response.statusCode);
    }
    return xml.blobParseListBlobs(response);
  });
};

/**
 * Establishes and manages a lock on a container for delete operations.
 * The lock duration can be 15 to 60 seconds, or can be infinite.
 *
 * @method leaseContainer
 * @param name - Name of the container
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    leaseId: '...',                   // GUID string; it is required in case of renew, change,
 *                                      // or release of the lease
 *    leaseAction: '...',               // Lease container operation. The possible values are: acquire, renew,
 *                                      // change, release, break (required)
 *    leaseBreakPeriod: '...',          // For a break operation, proposed duration the lease should continue
 *                                      // before it is broken, in seconds, between 0 and 60.
 *    leaseDuration: '...',             // Specifies the duration of the lease, in seconds, or negative one (-1)
 *                                      // for a lease that never expires. Required for `acquire` action.
 *    proposedLeaseId: '...'            // GUID string; Optional for `acquire`, required for `change` action.
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has been
 *                                      // modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has not been
 *                                      // modified since the specified date/time. (optional)
 * }
 * ```
 * @returns {Promise} A promise for an object on the form:
 * ```js
 * {
 *    leaseId: '...',             // The unique lease id.
 *    leaseTime: '...'            // Approximate time remaining in the lease period, in seconds.
 *    eTag: '...',                // The entity tag of the container
 *    lastModified: '...',        // The date/time the container was last modified
 * }
 * ```
 */
Blob.prototype.leaseContainer = function leaseContainer(name, options) {
  assert(typeof name === 'string', 'The name of the container must be specified and must be a string value.');
  assert(options, "options is required");
  var query = {
    restype: 'container',
    comp: 'lease'
  };

  var path = '/' + name;
  var headers = {};

  assert(options.leaseAction, "The `options.leaseAction` must be given");

  if (options.leaseId) {
    assert(utils.isValidGUID(options.leaseId), 'The supplied `leaseId` is not a valid GUID');
    headers['x-ms-lease-id'] = options.leaseId;
  }

  assert(
    options.leaseAction === 'acquire'
    || options.leaseAction === 'renew'
    || options.leaseAction === 'change'
    || options.leaseAction === 'release'
    || options.leaseAction === 'break',
    'The supplied `options.leaseAction` is not valid. The possible values are: acquire, renew, change, release, break'
  );
  headers['x-ms-lease-action'] = options.leaseAction;

  if((options.leaseAction === 'renew'
    || options.leaseAction === 'change'
    || options.leaseAction === 'release')
    && !options.leaseId) {
    throw new Error('The `options.leaseId` must be given if the `options.leaseAction` is `renew` or `change` or `release`');
  }

  if (options.leaseBreakPeriod){
    assert(Number.isInteger(options.leaseBreakPeriod) && (options.leaseBreakPeriod >= 0 || options.leaseBreakPeriod <= 60),
      'The `options.leaseBreakPeriod` is not valid; it should be a number between 0 and 60');
    headers['x-ms-lease-break-period'] = this.options.leaseBreakPeriod;
  }

  if(options.leaseAction === 'acquire' && !options.leaseDuration){
    throw new Error ('The `options.leaseDuration` must be given if the lease action is `acquire`');
  }

  if (options.leaseDuration) {
    assert(options.leaseDuration >= 15 && (options.leaseDuration <= 60 || options.leaseDuration === -1),
      'The `options.leaseDuration` must be a value between 15 and 60 or -1.');
    headers['x-ms-lease-duration'] = options.leaseDuration.toString();
  }

  if (options.leaseAction === 'change' && !options.proposedLeaseId) {
    throw new Error('The `options.proposedLeaseId` must be given if the lease action is `change`');
  }
  if(options.proposedLeaseId){
    assert(utils.isValidGUID(this.options.leaseId), 'The supplied `proposedLeaseId` is not a valid GUID');
    headers['x-ms-proposed-lease-id'] = options.proposedLeaseId;
  }

  utils.setConditionalHeaders(headers, options, true);

  return this.request('PUT', path, query, headers).then(function(response) {
    if (response.statusCode !== 200 && response.statusCode !== 201 && response.statusCode !== 202) {
      throw new Error("leaseContainer: Unexpected statusCode: " + response.statusCode);
    }

    var result = {
      leaseId: response.headers['x-ms-lease-id'],
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified'])
    };
    if (response.headers['x-ms-lease-time']) {
      result.leaseTime = response.headers['x-ms-lease-time'];
    }
    return result;
  });
};

/**
 * Creates a new block, page, or append blob, or updates the content of an existing block blob.
 * Updating an existing block blob overwrites any existing metadata on the blob,
 * and the content of the existing blob is overwritten with the content of the new blob.
 *
 * Note that a call to a putBlob to create a page blob or an append blob only initializes the blob.
 * To add content to a page blob, call the putPage. To add content to an append blob, call the appendBlock.
 *
 * @method putBlob
 * @param {string} container - Name of the container where the blob should be stored
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    metadata: '...',                          // Name-value pairs associated with the blob as metadata
 *    contentType: 'application/octet-stream',  // The MIME content type of the blob (optional)
 *    contentEncoding: '...',                   // Specifies which content encodings have been applied
 *                                              // to the blob. (optional)
 *    contentLanguage: '...',                   // Specifies the natural languages used by this resource(optional)
 *    cacheControl: '...',                      // The Blob service stores this value but does not
 *                                              // use or modify it. (optional)
 *    disableContentMD5Check: 'false',          // Enable/disable the content md5 check is disabled.(optional)
 *    type: BlockBlob|PageBlob|AppendBlob,      // The type of blob to create: block blob, page blob,
 *                                              // or append blob (required)
 *    leaseId: '...',                           // Lease id (required if the blob has an active lease)
 *    contentDisposition: '...',                // Specifies the content disposition of the blob (optional)
 *    ifModifiedSince: new Date(),              // Specify this to perform the operation only if the resource
 *                                              // has been modified since the specified time.
 *    ifUnmodifiedSince: new Date(),            // Specify this to perform the operation only if the resource
 *                                              // has not been modified since the specified date/time.
 *    ifMatch: '...',                           // ETag value. Specify this to perform the operation only if the
 *                                              // resource's ETag matches the value specified.
 *    ifNoneMatch: '...',                       // ETag value. Specify this to perform the operation only if the
 *                                              //resource's ETag does not match the value specified.
 *    pageBlobContentLength: '...',             // Specifies the maximum size for the page blob, up to 1 TB.
 *                                              // (required for page blobs)
 *    pageBlobSequenceNumber: 0,                // The sequence number - a user-controlled value that you can use
 *                                              // to track requests (optional, only for page blobs)
 * }
 *```
 * @param {string|buffer} content - The content of the blob
 * @return {Promise} A promise for an object on the form:
 * ```js
 * {
 *    eTag: '...',         // The entity tag of the blob
 *    lastModified: '...', // The date/time the blob was last modified
 *    contentMD5: '...',   // The MD5 hash of the blob
 * }
 * ```
 */
Blob.prototype.putBlob = function putBlob(container, blob, options, content) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');
  assert(options, "options is required");
  assert(options.type, 'The blob type must be specified');
  assert(options.type === 'BlockBlob'
    || options.type === 'PageBlob'
    || options.type === 'AppendBlob',
    'The blob type is invalid. The possible types are: BlockBlob, PageBlob or AppendBlob.');

  if (options.type === 'PageBlob' && content) {
    throw new Error('Do not include content when a page blob is created. Use putPage() to add/modify the content of a page blob');
  }
  if(options.type === 'AppendBlob' && content) {
    throw new Error('Do not include content when an append blob is created. Use appendBlock() to add content to the end of the append blob');
  }

  if ((options.type === 'BlockBlob'
    || options.type === 'AppendBlob')) {

    if (options.pageBlobContentLength) {
      throw new Error('Do not include page blob content length to a block blob or to an append blob.');
    }
    if (options.pageBlobSequenceNumber) {
      throw new Error('Do not include page blob sequence number to a block blob or to an append blob.');
    }
  }

  // check the content length
  var contentLength = 0;
  if (content && Buffer.isBuffer(content)) {
    contentLength = content.length;
  } else if (content) {
    contentLength = Buffer.byteLength(content);
  }

  if (options.type === 'BlockBlob'
    && contentLength > MAX_SINGLE_UPLOAD_BLOCK_BLOB_SIZE_IN_BYTES) {
    throw new Error('The maximum size of a block blob that can be uploaded with putBlob() is ' + MAX_SINGLE_UPLOAD_BLOCK_BLOB_SIZE_IN_BYTES + '.' +
      'In order to upload larger blobs, use putBlock() and putBlockList()');
  }
  if (options.type === 'PageBlob'){
    if (options.pageBlobContentLength % PAGE_SIZE !== 0) {
      throw new Error('Page blob length must be multiple of ' + PAGE_SIZE + '.');
    }
    if (options.pageBlobContentLength < MAX_PAGE_SIZE) {
      throw new Error('The maximum size of the page blob (options.pageBlobContentLength) is ' + MAX_PAGE_SIZE + '.');
    }
    if (options.pageBlobSequenceNumber && typeof options.pageBlobSequenceNumber !== 'number') {
      throw new Error('The `options.pageBlobSequenceNumber` is invalid. It must be a number');
    }
    if (options.pageBlobSequenceNumber
      && options.pageBlobSequenceNumber >= 0
      && options.pageBlobSequenceNumber < Math.pow(2, 63)) {
      throw new Error('The `options.pageBlobSequenceNumber` is invalid. It must be a number between 0 and 2^63 - 1');
    }
  }

  var query = {};
  var path = '/' + container + '/' + blob;
  var headers = {};

  headers['content-length'] = options.type === 'PageBlob' || options.type === 'AppendBlob' ? 0 : contentLength;

  if (options.contentType) {
    headers['content-type'] = options.contentType;
  }
  if (options.contentEncoding) {
    headers['content-encoding'] = options.contentEncoding;
  }
  if (options.contentLanguage) {
    headers['content-language'] = options.contentLanguage;
  }
  if (options.cacheControl) {
    headers['cache-control'] = options.cacheControl;
  }

  headers['x-ms-blob-type'] = options.type;

  if (!options.disableContentMD5Check && options.type === 'BlockBlob' && content) {
    headers['content-md5'] = utils.md5(content);
  }

  if (options.contentDisposition) {
    headers['x-ms-blob-content-disposition'] = options.contentDisposition;
  }

  // support for condition headers
  utils.setConditionalHeaders(headers, options);

  if (options.pageBlobContentLength) {
    headers['x-ms-blob-content-length'] = options.pageBlobContentLength;
  }
  if (options.pageBlobSequenceNumber) {
    headers['x-ms-blob-sequence-number'] = options.pageBlobSequenceNumber;
  }

  // add metadata
  if (options.metadata){
    for(var key in options.metadata) {
      if (options.metadata.hasOwnProperty(key)) {
        headers['x-ms-meta-' + key] = options.metadata[key];
      }
    }
  }

  return this.request('PUT', path, query, headers, content).then(function(response){
    if(response.statusCode !== 201) {
      throw new Error("putBlob: Unexpected statusCode: " + response.statusCode);
    }

    return {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified']),
      contentMD5: response.headers['content-md5']
    }
  });
};

/**
 * Reads or downloads a blob from the system, including its metadata and properties.
 *
 * @method getBlob
 * @param {string} container - Name of the container where the blob should be stored
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has been
 *                                      // modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has not
 *                                      // been modified since the specified date/time. (optional)
 *    ifMatch: '...',                   // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag matches the value specified. (optional)
 *    ifNoneMatch: '...',               // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag does not match the value specified. (optional)
 * }
 *```
 * @return {Promise} A promise for an object on the form:
 * ```js
 * {
 *    eTag: '...',                    // The entity tag of the blob
 *    lastModified: '...',            // The date/time the blob was last modified.
 *    contentType: '...',             // The content type specified for the blob
 *    contentMD5: '...',              // The MD5 hash fo the blob
 *    contentEncoding: '...',         // The content encoding of the blob
 *    contentLanguage: '...',         // The content language of the blob
 *    cacheControl: '...',            // The cache control of the blob
 *    contentDisposition: '...',      // The content disposition of the blob
 *    pageBlobSequenceNumber: '...',  // The current sequence number for a page blob.
 *    type: '...',                    // The blob type: block, page or append blob.
 *    blobCommittedBlockCount: '...', // The number of committed blocks present in the blob.
 *                                    // This is returned only for append blobs.
 *    metadata: '...',                // Name-value pairs associated with the blob as metadata
 *    content: '...'                  // The content
 * }
 * ```
 */
Blob.prototype.getBlob = function getBlob(container, blob, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  var query = {};
  var path = '/' + container + '/' + blob;
  var headers = {};

  utils.setConditionalHeaders(headers, options);

  return this.request('GET', path, query, headers).then(function (response) {
    if (response.statusCode !== 200) {
      throw new Error("getBlob: Unexpected statusCode: " + response);
    }
    var responseHeaders = response.headers;

    return {
      contentType: response.headers['content-type'],
      contentMD5: responseHeaders['content-md5'],
      contentEncoding: responseHeaders['content-encoding'],
      contentLanguage: responseHeaders['content-language'],
      cacheControl: responseHeaders['cache-control'],
      contentDisposition: responseHeaders['content-disposition'],
      pageBlobSequenceNumber: responseHeaders['x-ms-blob-sequence-number'],
      blobCommittedBlockCount: responseHeaders['x-ms-blob-committed-block-count'],
      metadata: utils.extractMetadataFromHeaders(response),
      type: responseHeaders['x-ms-blob-type'],
      eTag: responseHeaders['etag'],
      lastModified: new Date(responseHeaders['last-modified']),
      content: response.payload
    };
  });
};

/**
 * Returns all user-defined metadata, standard HTTP properties, and system properties for the blob.
 *
 * @method getBlobProperties
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has been
 *                                      // modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has not been
 *                                      // modified since the specified date/time. (optional)
 *    ifMatch: '...',                   // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag matches the value specified. (optional)
 *    ifNoneMatch: '...',               // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag does not match the value specified. (optional)
 * }
 *```
 *
 * @return {Promise} A promise for an object on the form:
 * ```js
 * {
 *    metadata: '...',                // Name-value pairs that correspond to the user-defined metadata
 *                                    // associated with this blob.
 *    lastModified: '...',            // The date/time the blob was last modified.
 *    type: '...',                    // The blob type
 *    leaseDuration: '...',           // When a blob is leased, specifies whether the lease is of
 *                                    // infinite or fixed duration
 *    leaseState: '...',              // Lease state of the blob
 *    leaseStatus: '...',             // The lease status of the blob.
 *    contentLength: '...',           // The size of the blob in bytes
 *    contentType: '...',             // The content type specified for the blob
 *    eTag: '...',                    // The blob Etag
 *    contentMD5: '...'               // The content-md5 of the blob
 *    contentEncoding: '...',         // The content encoding of the blob
 *    contentLanguage: '...'          // The content language of the blob
 *    contentDisposition: '...',      // The content disposition of the blob
 *    cacheControl: '...',            // The cache control of the blob
 *    pageBlobSequenceNumber: '...',  // The current sequence number for a page blob.
 *    committedBlockCount: '...',     // The number of committed blocks present in the blob (for append blob).
 * }
 * ```
 */
Blob.prototype.getBlobProperties = function getBlobProperties(container, blob, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  var query = {};
  var path = '/' + container + '/' + blob;
  var headers = {};

  utils.setConditionalHeaders(headers, options);

  return this.request('HEAD', path, query, headers).then(function (response) {
    if (response.statusCode !== 200) {
      throw new Error("getBlobProperties: Unexpected statusCode: " + response);
    }

    /**
     * TODO information about:
     * - copyCompletionTime,
     * - copyStatusDescription,
     * - copyStatusDescription,
     * - copyId,
     * - copyProgress,
     * - copySource,
     * - copyStatus,
     * - copyDestinationSnapshot
     * - incrementalCopy
     *
     * will be added after the copyBlob will be implemented
     */
    var result = {
      metadata: utils.extractMetadataFromHeaders(response),
      type: response.headers['x-ms-blob-type'],
      leaseState: response.headers['x-ms-lease-state'],
      leaseStatus: response.headers['x-ms-lease-status'],
      contentLength: response.headers['content-length'],
      contentType: response.headers['content-type'],
      eTag: response.headers['etag']
    };
    if (response.headers['last-modified']) {
      result.lastModified = new Date(response.headers['last-modified']);
    }
    if (response.headers['x-ms-lease-duration']) {
      result.leaseDuration = response.headers['x-ms-lease-duration'];
    }
    if (response.headers['content-md5']) {
      result.contentMD5 = response.headers['content-md5'];
    }
    if (response.headers['content-encoding']) {
      result.contentEncoding = response.headers['content-encoding'];
    }
    if (response.headers['content-language']) {
      result.contentLanguage = response.headers['content-language'];
    }
    if (response.headers['content-disposition']) {
      result.contentDisposition = response.headers['content-disposition'];
    }
    if (response.headers['content-control']) {
      result.cacheControl = response.headers['content-control'];
    }
    if (response.headers['x-ms-blob-sequence-number']) {
      result.pageBlobSequenceNumber = response.headers['x-ms-blob-sequence-number'];
    }
    if (response.headers['x-ms-blob-committed-block-count']) {
      result.committedBlockCount = response.headers['x-ms-blob-committed-block-count'];
    }
    return result;
  });
};

/**
 * Sets system properties on the blob
 *
 * @method setBlobProperties
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    cacheControl: '...',                      // The cache control string for the blob (optional)
 *                                              // If this property is not specified, then the property
 *                                              // will be cleared for the blob.
 *    contentType: '...',                       // The MIME content type of the blob (optional)
 *                                              // If this property is not specified, then the property
 *                                              // will be cleared for the blob.
 *    contentMD5: '...',                        // The MD5 hash of the blob (optional)
 *                                              // If this property is not specified, then the property
 *                                              // will be cleared for the blob.
 *    contentEncoding: '...',                   // The content encodings of the blob. (optional)
 *                                              // If this property is not specified, then the property
 *                                              // will be cleared for the blob.
 *    contentLanguage: '...',                   // The content language of the blob. (optional)
 *                                              // If this property is not specified, then the property
 *                                              // will be cleared for the blob.
 *    contentDisposition: '...',                // The content disposition (optional)
 *                                              // If this property is not specified, then the property
 *                                              // will be cleared for the blob.
 *    pageBlobContentLength: '...',             // The new size of a page blob. If the specified value is
 *                                              // less than the current size of the blob, then all pages
 *                                              // above the specified value are cleared.
 *                                              // This property applies to page blobs only.
 *    pageBlobSequenceNumberAction:
 *              'max|update|increment',         // Indicates how the service should modify the blob's
 *                                              // sequence number.
 *                                              // - max: Sets the sequence number to be the higher of the
 *                                              //        value included with the request and the value
 *                                              //        currently stored for the blob.
 *                                              // - update: Sets the sequence number to the value
 *                                              //           included with the request.
 *                                              // - increment: Increments the value of the sequence
 *                                              //              number by 1.
 *                                              // This property applies to page blobs only. (optional)
 *    pageBlobSequenceNumber: '...',            // The page blob sequence number.
 *                                              // Optional, but required if the
 *                                              // `pageBlobSequenceNumberAction` option is set to `max`
 *                                              // or `update`.
 *                                              // This property applies to page blobs only.
 *    ifModifiedSince: new Date(),              // Specify this to perform the operation only if the
 *                                              // resource has been modified since the specified time.
 *                                              // (optional)
 *    ifUnmodifiedSince: new Date(),            // Specify this to perform the operation only if the
 *                                              // resource has not been modified since the specified
 *                                              // date/time. (optional)
 *    ifMatch: '...',                           // ETag value. Specify this to perform the operation only
 *                                              // if the resource's ETag matches the value specified.
 *                                              // (optional)
 *    ifNoneMatch: '...',                       // ETag value. Specify this to perform the operation only
 *                                              // if the resource's ETag does not match the value
 *                                              // specified. (optional)
 * }
 *```
 *
 * @return {Promise} A promise for an object on the form:
 * ```js
 * {
 *      eTag: '...',               // The entity tag of the blob
 *      lastModified: '...',       // The date/time the blob was last modified
 *      blobSequenceNumber: '...', // The blob's current sequence number (if the blob is a page blob)
 * }
 * ```
 */
Blob.prototype.setBlobProperties = function setBlobProperties(container, blob, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  var query = {
    comp: 'properties'
  };
  var path = '/' + container + '/' + blob;
  var headers = {};

  if (options){
    if (options.cacheControl) headers['x-ms-blob-cache-control'] = options.cacheControl;
    if (options.contentType) headers['x-ms-blob-content-type'] = options.contentType;
    if (options.contentMD5) headers['x-ms-blob-content-md5'] = options.contentMD5;
    if (options.contentEncoding) headers['x-ms-blob-content-encoding'] = options.contentEncoding;
    if (options.contentLanguage) headers['x-ms-blob-content-language'] = options.contentLanguage;
    if (options.contentDisposition) headers['x-ms-blob-content-disposition'] = options.contentDisposition;
    if (options.pageBlobContentLength) headers['x-ms-blob-content-length'] = options.pageBlobContentLength;
    if (options.pageBlobSequenceNumberAction){
      assert(options.pageBlobSequenceNumberAction === 'max'
        || options.pageBlobSequenceNumberAction === 'update'
        || options.pageBlobSequenceNumberAction === 'increment',
        'The `options.pageBlobSequenceNumberAction` is invalid. The possible values are: max, update and increment.');
      headers['x-ms-sequence-number-action'] = options.pageBlobSequenceNumberAction;
      if (options.pageBlobSequenceNumberAction === 'max'
        || options.pageBlobSequenceNumberAction === 'update'
        && !options.pageBlobSequenceNumber) {
        throw new Error('If `options.pageBlobSequenceNumberAction` is `max` or `update`, the `options.pageBlobSequenceNumber` must be supplied.');
      }
      if (options.pageBlobSequenceNumber) headers['x-ms-blob-sequence-number'] = options.pageBlobSequenceNumber;
    }

    utils.setConditionalHeaders(headers, options);
  }

  return this.request('PUT', path, query, headers).then(function (response) {
    if (response.statusCode !== 200) {
      throw new Error("setBlobProperties: Unexpected statusCode: " + response);
    }

    var result = {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified'])
    };
    if (response.headers['x-ms-blob-sequence-number']) {
      result.blobSequenceNumber = response.headers['x-ms-blob-sequence-number'];
    }

    return result;
  });
};

/**
 * Get the metadata for the blob with the given name.
 *
 * Note, this is a `HEAD` request, so if the container is missing you get an
 * error with `err.statusCode = 404`, but `err.code` property will be
 * `ErrorWithoutCode`.
 *
 * @method getBlobMetadata
 * @param {string} container - the name of the container
 * @param {string} blob - the name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has been
 *                                      // modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has not
 *                                      // been modified since the specified date/time. (optional)
 *    ifMatch: '...',                   // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag matches the value specified. (optional)
 *    ifNoneMatch: '...',               // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag does not match the value specified. (optional)
 * }
 *```
 *
 * @returns {Promise} a promise for metadata key/value pair
 * A promise for an object on the form:
 * ```js
 * {
 *      eTag: '...',               // The entity tag of the blob
 *      lastModified: '...',       // The date/time the blob was last modified
 *      metadata: '...'            // Name-value pairs that correspond to the user-defined metadata
 *                                 // associated with this blob.
 * }
 * ```
 */
Blob.prototype.getBlobMetadata = function getBlobMetadata(container, blob, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  var query = {
    comp: 'metadata'
  }
  var path = '/' + container + '/' + blob;
  var headers = {};

  utils.setConditionalHeaders(headers, options);

  return this.request('HEAD', path, query, headers).then(function(response) {
    if (response.statusCode !== 200) {
      throw new Error("getBlobMetadata: Unexpected statusCode: " + response.statusCode);
    }
    var result = {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified']),
      metadata: utils.extractMetadataFromHeaders(response)
    };
    return result;
  });
};

/**
 * Sets metadata for the specified blob.
 * Overwrites all existing metadata that is associated with that blob.
 *
 * @method setBlobMetadata
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} metadata - Mapping from metadata keys to values.
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has been
 *                                      // modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has not
 *                                      // been modified since the specified date/time. (optional)
 *    ifMatch: '...',                   // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag matches the value specified. (optional)
 *    ifNoneMatch: '...',               // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag does not match the value specified. (optional)
 * }
 *```
 * @returns {Promise} A promise for an object on the form:
 * ```js
 * {
 *      eTag: '...',               // The entity tag of the blob
 *      lastModified: '...'        // The date/time the blob was last modified.
 * }
 * ```
 */
Blob.prototype.setBlobMetadata = function setBlobMetadata(container, blob, metadata, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  // Construct query string
  var query = {
    comp: 'metadata'
  };
  // Construct headers
  var headers = {};

  utils.setConditionalHeaders(headers, options);

  for(var key in metadata) {
    if (metadata.hasOwnProperty(key)) {
      headers['x-ms-meta-' + key] = metadata[key];
    }
  }
  var path = '/' + container + '/' + blob;
  return this.request('PUT', path, query, headers).then(function(response) {
    if(response.statusCode !== 200) {
      throw new Error('setBlobMetadata: Unexpected statusCode: ' + response.statusCode);
    }

    return {
      eTag: response.headers.etag,
      lastModified: new Date(response.headers['last-modified'])
    }
  });
};

/**
 * Marks the specified blob for deletion. The blob is later deleted during garbage collection.
 *
 * @method deleteBlob
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has been
 *                                      // modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has not been
 *                                      // modified since the specified date/time. (optional)
 *    ifMatch: '...',                   // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag matches the value specified. (optional)
 *    ifNoneMatch: '...',               // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag does not match the value specified. (optional)
 * }
 *```
 * @return {Promise} A promise that container has been marked for deletion.
 */
Blob.prototype.deleteBlob = function deleteBlob(container, blob, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  var query = {};
  var path = '/' + container + '/' + blob;
  var headers = {};

  utils.setConditionalHeaders(headers, options);

  return this.request('DELETE', path, query, headers).then(function(response) {
    if(response.statusCode !== 202) {
      throw new Error('deleteBlob: Unexpected statusCode: ' + response.statusCode);
    }
  });
};

/**
 * Creates a new block to be committed as part of a blob.
 *
 * @method putBlock
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    blockId: '...',                  // A valid Base64 string value that identifies the block
 *                                     // For a given blob, the length of the value specified for the
 *                                     // blockId must be the same size for each block.(required)
 *    disableContentMD5Check: 'false', // Enable/disable the content md5 check is disabled.(optional)
 * }
 * ```
 * @param {string|buffer} content - The content of the block
 *
 * @returns {Promise}  A promise for an object on the form:
 * ```js
 * {
 *    contentMD5: '...'   // The MD5 hash of the block
 * }
 */
Blob.prototype.putBlock = function putBlock(container, blob, options, content) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');
  assert(content, 'The content must be specified');
  assert(options, 'options is required');
  assert(options.blockId, 'The block identifier must be specified');

  var blockIdLength = Buffer.byteLength(new Buffer(options.blockId, 'base64'));
  assert(blockIdLength <= 64, 'The block id is invalid. It must be less than or equal to 64 bytes in size.');

  var contentLength = 0;
  if (content && Buffer.isBuffer(content)) {
    contentLength = content.length;
  } else if (content){
    contentLength = Buffer.byteLength(content);
  }
  if (contentLength > MAX_BLOCK_SIZE) {
    throw new Error('The maximum size of a block is ' + MAX_BLOCK_SIZE + '.');
  }

  var query = {
    comp: 'block',
    blockid: options.blockId
  };

  var path = '/' + container + '/' + blob;
  var headers = {};
  headers['content-length'] = contentLength;
  if (options && !options.disableContentMD5Check){
    headers['content-md5'] = utils.md5(content);
  }

  return this.request('PUT', path, query, headers, content).then(function(response) {
    if(response.statusCode !== 201) {
      throw new Error('putBlock: Unexpected statusCode: ' + response.statusCode);
    }

    return {
      contentMD5: response.headers['content-md5']
    }
  });
};

/**
 * Writes a blob by specifying the list of block IDs that make up the blob.
 * In order to be written as part of a blob, a block must have been successfully written
 * to the server in a prior putBlock operation.
 *
 * @method putBlockList
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    cacheControl: '...',              // Blob's cache control (optional)
 *    contentType: '...',               // Blob's content type (optional)
 *    contentEncoding: '...',           // Blob's content encoding (optional)
 *    contentLanguage: '...',           // Blob's content language (optional)
 *    metadata: '...',                  // Name-value pairs that correspond to the user-defined metadata
 *                                      // associated with this blob.
 *    contentDisposition: '...',        // Blob's content disposition
 *    ifModifiedSince: new Date(),      // Specify this to perform the operation only if the resource has been
 *                                      // modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),    // Specify this to perform the operation only if the resource has not been
 *                                      // modified since the specified date/time. (optional)
 *    ifMatch: '...',                   // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag matches the value specified. (optional)
 *    ifNoneMatch: '...',               // ETag value. Specify this to perform the operation only if the resource's
 *                                      // ETag does not match the value specified. (optional)
 *    committedBlockIds: [],            // List of block ids to indicate that the Blob service should search only
 *                                      // the committed block list for the named blocks(optional)
 *    uncommittedBlockIds: [],          // List of block ids to indicate that the Blob service should search only
 *                                      // the uncommitted block list for the named blocks (optional)
 *    latestBlockIds: [],               // List of block ids to indicate that the Blob service should first
 *                                      // search the uncommitted block list. If the block is found in the
 *                                      // uncommitted list, that version of the block is the latest and should
 *                                      // be written to the blob.
 *                                      // If the block is not found in the uncommitted list, then the service
 *                                      // should search the committed block list for the named block and write
 *                                      // that block to the blob if it is found. (optional)
 * }
 *
 * @return {Promise} - A promise for an object on the form:
 * ```js
 * {
 *    eTag: '...',         // The entity tag of the blob
 *    lastModified: '...', // The date/time the blob was last modified.
 * }
 */
Blob.prototype.putBlockList = function putBlockList(container, blob, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  var query = {
    comp: 'blocklist'
  };

  var path = '/' + container + '/' + blob;
  var headers = {};

  // TODO content-md5 check
  if (options) {
    var data = '<?xml version="1.0" encoding="utf-8"?>';
    data += '<BlockList>';
    if (options.committedBlockIds){
      for(var i = 0; i < options.committedBlockIds.length; i++){
        data += '<Committed>' + options.committedBlockIds[i] + '</Committed>';
      }
    }

    if (options.uncommittedBlockIds){
      for(var i = 0; i < options.uncommittedBlockIds.length; i++){
        data += '<Uncommitted>' + options.uncommittedBlockIds[i] + '</Uncommitted>';
      }
    }

    if (options.latestBlockIds){
      for(var i = 0; i < options.latestBlockIds.length; i++){
        data += '<Latest>' + options.latestBlockIds[i] + '</Latest>';
      }
    }
    data += '</BlockList>';

    if (options.cacheControl){
      headers['x-ms-blob-cache-control'] = options.cacheControl;
    }
    if (options.contentType) {
      headers['x-ms-blob-content-type'] = options.contentType;
    }
    if (options.contentEncoding) {
      headers['x-ms-blob-content-encoding'] = options.contentEncoding;
    }
    if (options.contentLanguage) {
      headers['x-ms-blob-content-language'] = options.contentLanguage;
    }
    if (options.metadata) {
      for(var key in options.metadata) {
        if (options.metadata.hasOwnProperty(key)) {
          headers['x-ms-meta-' + key] = options.metadata[key];
        }
      }
    }
    if (options.contentDisposition) {
      headers['x-ms-blob-content-disposition'] = options.contentDisposition;
    }
    utils.setConditionalHeaders(headers, options);
  }

  return this.request('PUT', path, query, headers, data).then(function(response) {
    if(response.statusCode !== 201) {
      throw new Error('putBlockList: Unexpected statusCode: ' + response.statusCode);
    }
    return {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified']),
    }
  });
};

/**
 * Retrieves the list of committed list blocks (that that have been successfully committed to a given blob with
 * putBlockList()), and uncommitted list blocks (that have been uploaded for a blob using Put Block, but that have
 * not yet been committed)
 *
 * @method getBlockList
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *    blockListType: 'committed'  // Specifies whether to return the list of committed blocks, the list of
 *                                // uncommitted blocks, or both lists together. Valid values are committed,
 *                                // uncommitted, or all
 * }
 *```
 * @return {Promise} A promise for an object on the form:
 * ```js
 * {
 *    eTag: '...',
 *    committedBlocks: [
 *      {
 *        blockId: '...',     // Base64 encoded block identifier
 *        size: '...'         // Block size in bytes
 *      }
 *    ],
 *    uncommittedBlocks: [
 *    {
 *        blockId: '...',     // Base64 encoded block identifier
 *        size: '...'         // Block size in bytes
 *      }
 *   ]
 * }
 * ```
 */
Blob.prototype.getBlockList = function getBlockList(container, blob, options) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');

  var query = {
    comp: 'blocklist'
  };
  if (options && options.blockListType){
    if(options.blockListType !== 'committed'
      && options.blockListType !== 'uncommitted'
      && options.blockListType !== 'all') {
      throw new Error('The `options.blockListType` is invalid. The possible values are: committed, uncommitted and all');
    }
    query.blocklisttype = options.blockListType;
  }

  var path = '/' + container + '/' + blob;
  var headers = {};

  return this.request('GET', path, query, headers).then(function(response) {
    if(response.statusCode !== 200) {
      throw new Error('getBlockList: Unexpected statusCode: ' + response.statusCode);
    }

    var result = xml.blobParseListBlock(response);

    // The ETag is returned only if the blob has committed blocks
    if (response.headers['ETag']) {
      result.eTag = response.headers['ETag'];
    }

    return result;
  });
};

/**
 * Generates a base64 string that identifies a block.
 *
 * @method getBlockId
 * @param {string} prefix - the prefix of the block id
 * @param {number} blockNumber - the block number
 * @param {number} length - length of the block id
 *
 * @return {string} - a base64 string as a block identifier
 */
Blob.prototype.getBlockId = function getBlockId(prefix, blockNumber, length) {
  assert (typeof prefix === 'string', 'prefix must be specified and must be a string value and must be a string value.');
  assert (typeof blockNumber === 'number', 'blockNumber must be specified and must be a number.');
  assert (length, 'The block id length must be specified');

  var paddingStr = blockNumber + '';
  while (paddingStr.length < length){
    paddingStr = '0' + paddingStr;
  }
  return new Buffer(prefix + '-' + paddingStr).toString('base64');
};

/**
 * Commits a new block of data to the end of an existing append blob.
 *
 * @method appendBlock
 * @param {string} container - Name of the container
 * @param {string} blob - Name of the blob
 * @param {object} options - Options on the following form
 * ```js
 * {
 *
 *    disableContentMD5Check: 'false',          // Enable/disable the content md5 check is disabled.(optional)
 *    blobConditionMaxSize: '...',              // The max length in bytes permitted for the append blob (optional)
 *    blobConditionAppendPositionOffset: '...', // A number indicating the byte offset to compare (optional)
 *    ifModifiedSince: new Date(),              // Specify this to perform the operation only if the resource has
 *                                              // been modified since the specified time. (optional)
 *    ifUnmodifiedSince: new Date(),            // Specify this to perform the operation only if the resource has
 *                                              // not been modified since the specified date/time. (optional)
 *    ifMatch: '...',                           // ETag value. Specify this to perform the operation only if the
 *                                              // resource's ETag matches the value specified. (optional)
 *    ifNoneMatch: '...',                       // ETag value. Specify this to perform the operation only if the
 *                                              // resource's ETag does not match the value specified. (optional)
 * }
 *```
 * @param {string|buffer} content - the content of the block
 *
 * @return {Promise} A promise for an object on the form:
 * ```js
 * {
 *    eTag: '...',                // The entity tag for the append blob
 *    lastModified: '...',        // The date/time the blob was last modified
 *    contentMD5: '...',          // The MD5 hash of the append blob
 *    appendOffset: '...',        // The offset at which the block was committed, in bytes.
 *    committedBlockCount: '...', // The number of committed blocks present in the blob.
 *                                // This can be used to control how many more appends can be done.
 * }
 * ```
 */
Blob.prototype.appendBlock = function appendBlock(container, blob, options, content) {
  assert(typeof container === 'string', 'The name of the container must be specified and must be a string value.');
  assert(typeof blob === 'string', 'The name of the blob must be specified and must be a string value.');
  assert(content, 'The content of block must be specified');

  var contentLength = 0;
  if (content && Buffer.isBuffer(content)) {
    contentLength = content.length;
  } else if (content){
    contentLength = Buffer.byteLength(content);
  }
  if (contentLength > MAX_APPEND_BLOCK_SIZE) {
    throw new Error('The maximum size of an append block is ' + MAX_APPEND_BLOCK_SIZE + '.');
  }
  var query = {
    comp: 'appendblock'
  };

  var path = '/' + container + '/' + blob;
  var headers = {};
  headers['content-length'] = contentLength;
  if (options) {
    if (!options.disableContentMD5Check) {
      headers['content-md5'] = utils.md5(content);
    }
    if (options.blobConditionMaxSize) {
      headers['x-ms-blob-condition-maxsize'] = options.blobConditionMaxSize;
    }
    if (options.blobConditionAppendPositionOffset) {
      assert(typeof options.blobConditionAppendPositionOffset === 'number',
        'The `options.blobConditionAppendPositionOffset` must be a number');
      headers['x-ms-blob-condition-appendpos'] = options.blobConditionAppendPositionOffset;
    }
    utils.setConditionalHeaders(headers, options);
  }

  return this.request('PUT', path, query, headers, content).then(function(response) {
    if(response.statusCode !== 201) {
      throw new Error('appendBlock: Unexpected statusCode: ' + response.statusCode);
    }

    return {
      eTag: response.headers['etag'],
      lastModified: new Date(response.headers['last-modified']),
      contentMD5: response.headers['content-md5'],
      appendOffset: response.headers['x-ms-blob-append-offset'],
      committedBlockCount: response.headers['x-ms-blob-committed-block-count']
    };
  });
};