text: //
  // Copyright (c) Microsoft and contributors.  All rights reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //   http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  //
  // See the License for the specific language governing permissions and
  // limitations under the License.
  //
  
  /**
   * Implements uploading of a page blob featuring
   * - conversion of non-fixed VHDs to fixed VHDs on the fly
   * - multiple parameter-controlled simultaneous uploads
   * - fast speed
   * - simultaneous MD5 hash computation (if CPU is not fully utilized by upload)
   * - error retry
   * - incremental upload
   * - support for using base (aka parent) VHD in the cloud to speed up difference VHD uploads
   */
  
  var assert = require('assert');
  var fs = require('fs');
  var util = require('util');
  var crypto = require('crypto');
  var azureCommon = require('azure-common');
  var azureStorage = require('azure-storage');
  
  var jobTracker = require('./jobTracker');
  var intSet = require('./intSet');
  var BitArray = require('../../../../util/bitArray');
  var blobInfo = require('./blobInfo');
  var callbackAggregator = require('../../../../util/callbackAggregator');
  var utils = require('../../../../util/utils');
  
  var blobUtils = require('../../../../util/blobUtils');
  var splitDestinationUri = blobUtils.splitDestinationUri;
  var splitBlobResourceName = blobUtils.splitBlobResourceName;
  var BlobConstants = azureCommon.Constants.BlobConstants;
  
  var http = require('http');
  http.globalAgent.maxSockets = Math.max(2048, http.globalAgent.maxSockets);
  
  // compare arrays, strings or buffers
  function compare(a, b) {
    if (!a && !b) {
      return true;
    }
    if (!a || !b || a.length !== b.length) {
      return false;
    }
    for (var i = 0; i < a.length; ++i) {
      if (a[i] !== b[i]) {
        return false;
      }
    }
    return true;
  }
  
  // implement right-justification for formatted output
  function extendLeftTo(string, size, fixed) {
    var str = fixed !== undefined ? string.toFixed(fixed) : string.toString();
    var newStr = '              ' + str;
    return newStr.slice(-Math.max(size, str.length));
  }
  
  function findNonZeroBlocks(fd, blockSize, callback) {
    var fileSize = fs.fstatSync(fd).size;
    var numBlocks = Math.ceil(fileSize / blockSize);
    
    var nonZeroBlocks = new BitArray(numBlocks);
    var buf = new Buffer(blockSize);
    readFileBlock(0);
  
    function readFileBlock(blockIndex) {
      var fileOffset = blockIndex * blockSize;
      var len = Math.min(blockSize, fileSize - fileOffset);
      if (len <= 0)
        return callback(null, nonZeroBlocks);
  
      fs.read(fd, buf, 0, len, fileOffset, readCallback);
  
      function readCallback(err, bytesRead) {
        if (err)
          return callback(err);
  
        var isNonZero = false;
        for (var i = 0; i < bytesRead; i++) {
          if (buf[i]) {
            isNonZero = true;
            break;
          }
        }
  
        if (isNonZero || bytesRead == len) {
          nonZeroBlocks.set(blockIndex, isNonZero);
          readFileBlock(blockIndex + 1);
        } else {
          len -= bytesRead;
          fileOffset += bytesRead;
          fs.read(fd, buf, 0, len, fileOffset, readCallback);        
        }
      }
    }
  }
  
  exports.copyBlob = function copyBlob(sourceUri, sourceAccountKey, destinationUri, destinationAccountKey, callback) {
  
    var splitDestination = splitDestinationUri(destinationUri);
    if (!splitDestination.accountName || !splitDestination.blobName) {
      throw new Error('Destination is not well formed :' + destinationUri);
    }
  
    if (sourceAccountKey) {
      sourceUri = signBlobUri(sourceUri, sourceAccountKey);
    }
  
    // Using default host provided by Node SDK if not specified
    var blobService = utils.createBlobService(splitDestination.accountName, destinationAccountKey, splitDestination.host);
    var container = splitDestination.container || '$root';
    blobService.createContainerIfNotExists(container, function(error1, containerCreated, response1) {
      if (error1) {
        callback(error1, null, response1);
        return;
      }
  
      blobService.startCopyBlob(sourceUri, container, splitDestination.blobName, checkStatus);
      function checkStatus(error, blob, response) {
        if (!error) {
          var status = blob.copy.status;
          var copyStatusMessage = '  Copying image\r';
          if (response.headers && response.headers.hasOwnProperty('x-ms-copy-progress')) {
            var copyProgress = response.headers['x-ms-copy-progress'].split('/');
            if (copyProgress.length == 2) {
              if (copyProgress[0] > 0 && copyProgress[1] > 0) {
                copyStatusMessage = '  Copying image: ' +
                  Math.floor((copyProgress[0]*100.0 / copyProgress[1])) + '% (' +
                  response.headers['x-ms-copy-progress'] + ') \r';
              }
            }
          }
  
          fs.writeSync(1, copyStatusMessage);
          if (status.toLowerCase() !== 'success') {
            setTimeout(function() {
              blobService.getBlobProperties(container, splitDestination.blobName, checkStatus);
            }, 3000);
            return;
          }
        }
        callback(error, blob, response);
      }
    });
  };
  
  var signBlobUri = exports.signBlobUri = function signBlobUri(uri, storageAccountKey) {
    var splitUri = splitDestinationUri(uri);
    if (!splitUri.accountName || !splitUri.blobName) {
      throw new Error('The uri is not well formed :' + uri);
    }
  
    var leftPadTwo = function (n) {
      return (n < 10 ? '0' : '') + n;
    };
  
    function _toUTC(date) {
      return (date.getUTCFullYear()+'-' +
        leftPadTwo((date.getUTCMonth()+1))+'-' +
        leftPadTwo(date.getUTCDate())+'T' +
        leftPadTwo(date.getUTCHours())+':' +
        leftPadTwo(date.getUTCMinutes())+':' +
        leftPadTwo(date.getUTCSeconds())+'Z');
    }
  
    if (storageAccountKey) {
      var startDate = new Date();
      startDate.setMinutes(startDate.getMinutes() - 2);
      var expiryDate = new Date(startDate);
      expiryDate.setMinutes(startDate.getMinutes() + 60);
      var blobService = azureStorage.createBlobService(splitUri.accountName, storageAccountKey);
      var sharedAccessPolicy = {
          Id: null,
          AccessPolicy: {
            Permissions: BlobConstants.SharedAccessPermissions.READ,
            Start: _toUTC(startDate),
            Expiry: _toUTC(expiryDate)
          }
        };
      var queryString = blobService.generateSharedAccessSignature(splitUri.container, splitUri.blobName, sharedAccessPolicy);
      uri = uri + '?' + queryString;
    }
  
    return uri;
  };
  
  // UploadPageBlob()
  //
  // destinationUri may or may not contain the full path
  // http:// prefix is optional
  // Example:
  //
  // myaccount/myfilename.ext
  // http://myaccout/myDir/myfile
  // http://myAccount.blob.core.azure-preview.com/mydir/myfile
  //
  // port number and GET parameters are not supported
  exports.uploadPageBlob = function uploadPageBlob(destinationUri, accountKey, fileName, options, callback) {
  
    var splitDestination = splitDestinationUri(destinationUri);
    if (!splitDestination.accountName) {
      throw new Error('Destination is not well formed :' + destinationUri);
    }
  
    var blobService = utils.createBlobService(splitDestination.accountName, accountKey, splitDestination.host);
    uploadPageBlobFromBlobService(blobService, splitDestination.resourceName, fileName, options, callback);
  };
  
  exports.deleteBlob = function deleteBlob(destinationUri, accountKey, callback) {
    var splitDestination = splitDestinationUri(destinationUri);
    if (!splitDestination.accountName) {
      throw new Error('Destination is not well formed :' + destinationUri);
    }
  
    var blobService = utils.createBlobService(splitDestination.accountName, accountKey, splitDestination.host);
    var splitName = splitBlobResourceName(splitDestination.resourceName);
    blobService.deleteBlob(splitName.container, splitName.blobName, callback);
  };
  
  var uploadPageBlobFromBlobService =
    exports.uploadPageBlobFromBlobService =
      function uploadPageBlobFromBlobService(blobService, blobResourceName, fileName, options, callback) {
  
    var logger = options.logger || console;
    var logFunc = logger.info || console.log;
    var warnFunc = logger.warn || console.warn;
    var errorFunc = logger.error || console.error;
    var doNothing = function() {};
    var verboseFunc = logger.verbose || (options.verbose ? logFunc : doNothing);
    var sillyFunc =  logger.silly || doNothing;
    var exitWithError = options.exitWithError || function() {
      throw new Error(util.inspect(arguments));
    };
  
    var skipLine = options.skipLine || function() {
      fs.writeSync(1, '\n'); // just like console.log('');
    };
  
    verboseFunc('Uploading page blob ' + fileName);
    verboseFunc(' to ' +
        blobService.protocol + blobService.storageAccount + '/' + blobService.host +
        '/' + blobResourceName);
    var startTime = new Date();
  
    if (!callback) {
      callback = function(error) {
        if (error) {
          errorFunc(util.inspect(error));
        }
      };
    }
  
    var numThreads = options.threads || 128;
    numThreads = Math.floor(numThreads);
    if (numThreads < 1 || numThreads.toString() === 'NaN') {
      throw new Error('Number of parallel connections should be a positive number : '+ options.threads);
    }
  
    var splitName = splitBlobResourceName(blobResourceName);
    var container = splitName.container;
    var blobName = splitName.blobName;
  
    var size = fs.statSync(fileName).size;
    verboseFunc('Max parallel connections = ' + numThreads);
    verboseFunc('File size = ' + size);
    if (size === 0) {
      throw new Error('File cannot be read or has zero size: ' + fileName);
    }
    if (size % 512) {
      throw new Error('file size is not a multiple of 512 (not yet implemented) :' + size);
    }
  
    var blockSize = Math.min(2 * 1024 * 1024, BlobConstants.DEFAULT_WRITE_BLOCK_SIZE_IN_BYTES);
    blockSize -= (blockSize % 512);
    var dynVHD = false;
    var uploadSize = size;
    var uploadSizeWithFooter = uploadSize;
    var vhdInfo = null;
    if (options.vhd) {
      vhdInfo = require('./vhdTools').getVHDInfo(fileName);
      if (vhdInfo.ddHeader) { // dynamic or difference
        dynVHD = true;
        size = vhdInfo.footer.currentSize;
        if (vhdInfo.blocksSize !== size) {
          warnFunc('The total size of blocks ' + vhdInfo.blocksSize / 1024 / 1024 + ' Mb is different');
          warnFunc('  from declared VHD size ' + size / 1024 / 1024 + ' Mb');
          size = vhdInfo.blocksSize;
        }
        size += 512; // add footer
        verboseFunc('Dynamic or difference VHD: converting to fixed type');
        verboseFunc('Fixed VHD size = ' + size / 1024 + ' KB');
        var curSize = blockSize;
        blockSize = vhdInfo.ddHeader.blockSize; // should be vhdInfo.ddHeader.blockSize, should divide it
        while (blockSize > curSize) {
          blockSize /= 2;
        }
  
        if (vhdInfo.fullIndices) {
          uploadSize = vhdInfo.fullIndices.length * vhdInfo.ddHeader.blockSize;
          uploadSizeWithFooter = uploadSize + 512; // add footer
        } else {
          warnFunc('Warning: difference VHD with fixed [grand]parent ' + fileName);
          uploadSize -= 512; // without footer
        }
      }
  
      var sizeMB = size / 1024 / 1024;
      logFunc('VHD size : ' + ((sizeMB >= 10240) ?
        (sizeMB / 1024).toFixed() + ' GB'
        : sizeMB.toFixed() + ' MB'));
    }
  
    logFunc('Uploading ' + uploadSizeWithFooter / 1024 + ' KB');
  
    while (uploadSize / blockSize < numThreads / 2 && blockSize > 128 * 1024) {
      blockSize /= 2;
    }
  
    verboseFunc('Blob container = ' + container);
    verboseFunc('Blob name = ' + blobName);
    verboseFunc('VHD block size = ' + blockSize / 1024 + ' KB');
  
    var sizeExt = size + 511 - (size + 511) % 512;
  
    var maxIndex = Math.ceil((uploadSize / blockSize) - 1);
    var uploadBlocksPerBATBlocks = dynVHD ? vhdInfo.ddHeader.blockSize / blockSize : null;
  
    var maxRequest = maxIndex + dynVHD;
  
    // check if we need incremental upload
  
    var uploadedRegions = null;
    // need to finish two things to succeed: md5 (sometimes) and upload (always)
    // use callbackAggregator to track it, without tracking errors for upload
    var md5Agg = callbackAggregator.callbackAndAggregator(function(finished) {
      var alreadyExisted = finished.upload[1]; // arg 1
      verboseFunc(alreadyExisted ? 'Done - nothing to upload.' : 'Done!');
      callback(null, alreadyExisted);
    });
  
    var md5AggUpload = md5Agg.getCallback('upload');
  
    var skipMd5 = options.skipMd5;
  
    if (options.force) {
      // create a container if does not exist only
      blobInfo.getBlobInfo(blobService, container, null, null, true, onGotBlobInfo);
    } else {
      // see if we can do incremental upload
      blobInfo.getBlobInfo(blobService, container, blobName, {start : size - 512, end : size - 1}, true, onGotBlobInfo);
    }
  
    function onGotBlobInfo(error, result) {
      if (error) {
        errorFunc('Cannot access blob /' + blobResourceName +
            ' or its properties : ' + error.code);
        exitWithError(error);
      } else {
        if (result) {
          uploadedRegions = result.pageRegions;
          if (uploadedRegions && uploadedRegions.length === 0) {
            uploadedRegions = null;
          }
          if (uploadedRegions && result.blobProperties &&
              result.blobProperties.contentLength == size) {
            sillyFunc(''); // skip line for visibility in 'silly' mode
            verboseFunc('Uploading incrementally');
            if (result.blobProperties && result.blobProperties.contentMD5) {
              verboseFunc('Using pre-set MD5 = ' + result.blobProperties.contentMD5);
              skipMd5 = true;
            }
            // check footer (if vhd)
            if (dynVHD) {
              if (!compare(vhdInfo.footer.buffer, result.data)) {
                exitWithError((result.data && result.data.length > 0 ? '' : 'Couldn\'t read cloud blob footer. ') +
                    'Files are different - cannot upload incrementally. Use -f option to overwrite.');
              }
            }
            onBlobCreated();
            return;
          }
        }
        uploadedRegions = null;
      }
      if (options.parentBlob) {
        verboseFunc('Parent blob: ' + options.parentBlob);
        if (!vhdInfo || !vhdInfo.isDiff) {
          exitWithError('Error: base VHD specified for non-difference type VHD: type = ' + vhdInfo.footer.diskType);
        }
        createDiffVHDPageBlob(blobService, container, blobName, options.parentBlob, options.threads, vhdInfo, onBlobCreated);
      } else {
        blobService.createPageBlob(container, blobName, sizeExt,
          function onCreate(error) {
            if (error) {
              var errorCode = typeof(error) === 'object' ? error.code : undefined;
              errorFunc('Error: blobService.createPageBlob() ended with error code = ' + errorCode);
              if (!errorCode) errorFunc(util.inspect(error));
              callback(error);
              return;
            }
            onBlobCreated();
          });
      }
    }
  
    function createDiffVHDPageBlob(blobService, container, blobName, baseUri, threads, vhdInfo, callback) {
      var splitParentUri = splitDestinationUri(baseUri);
      blobService.startCopyBlob(splitParentUri.container, splitParentUri.blobName, container, blobName, function(error) {
        if (error) {
          errorFunc('Cannot copy or invalid blob: ' + baseUri + '\nError code ' + error.code);
          errorFunc(util.inspect(error));
          return;
        }
        var footerRegion = {start : size - 512, end : size - 1};
        blobInfo.getBlobInfo(blobService, container, blobName, null, false, function(error, result) {
          if (error || !result) {
            errorFunc(util.inspect(error));
            callback(error); // nothing to do here
            return;
          }
          var deleteRegions = [footerRegion];
          uploadedRegions = result.pageRegions;
          var regionsSet = intSet.createIntSet(uploadedRegions); // this does not copy regions!
          var blockSize = vhdInfo.ddHeader.blockSize;
  
          function process(index) {
            var start = blockSize * index;
            var end = start + blockSize - 1;
            if (regionsSet.intersects(start, end)) {
              // For simplicity and possibly speed,
              // delete the whole block, rather then some uploaded parts
              deleteRegions.push({start : start, end : end});
            }
          }
  
          if (vhdInfo.bat) { // This is not a Windows file name! :) 'bat' stands for VHD 'Block Allocation Table'
            for (var j in vhdInfo.bat) {
              process(j);
            }
          } else {
            for(var jj = vhdInfo.ddHeader.maxTableEntries; --jj >= 0;) {
              process(jj);
            }
          }
  
          threads = threads || 128;
          threads = Math.min(threads, deleteRegions.length);
          var next = threads;
          var running = threads;
          for (var i = 0;  i < threads; ++i) {
            deleteRegion(i);
          }
          var exiting = false;
  
          function deleteRegion(i) {
            if (exiting) {
              return;
            }
            blobService.clearPageRange(container, blobName, deleteRegions[i].start, deleteRegions[i].end, function(error) {
              if (error) {
                errorFunc('Cannot delele blob regions ' +
                    util.inspect(deleteRegions[i]));
                errorFunc(util.inspect(error));
                exiting = true;
                callback(error);
                return;
              }
              regionsSet.subtractInterval(deleteRegions[i].start, deleteRegions[i].end);
              next++;
              if (next >= deleteRegions.length) {
                if (--running <= 0) {
                  verboseFunc('Successfully copied and cleaned up base VHD');
                  onDeleted();
                }
                return;
              }
              deleteRegion(next);
            });
          }
  
          function onDeleted() {
            if (result.blobProperties && result.blobProperties.contentMD5) {
              result.blobProperties.contentMD5 = null; // clear it from the blob
              var blobOptions = { contentMD5: null, contentType: 'application/octet-stream' };
              blobService.setBlobProperties(container, blobName, blobOptions, function (error) {
                if (error) {
                  skipLine();
                  exitWithError(error);
                }
                callback(null, result);
              });
            } else {
              callback(null, result);
            }
          }
        });
      });
    }
  
    var md5Done = 0;
    var exiting = false;
  
    var md5AggMd5 = null;
  
    function calculateMD5(callback) {
      md5AggMd5 = md5Agg.getCallback('md5');
      var md5hash = crypto.createHash('md5');
      var stream =
        options.vhd ? vhdInfo.getReadStream() : fs.ReadStream(fileName);
      stream.on('data', function (data) {
        if (exiting) {
          verboseFunc('Waiting to finish...');
          stream.destroy();
          return;
        }
        md5hash.update(data);
        md5Done += data.length;
      });
      stream.on('end', function () {
        if (exiting) {
          return;
        }
        var hash = md5hash.digest('base64');
        callback(hash);
      });
      stream.on('error', function (e) {
        callback(null, e);
      });
    }
  
    function onMD5Calculated(md5, error) {
      if (error) {
        errorFunc('Cannot calculate MD5 or read file ' + fileName);
        exitWithError(error);
      }
      var blobOptions = { contentMD5: md5, contentType: 'application/octet-stream' };
      blobService.setBlobProperties(container, blobName, blobOptions, function (error) {
        if (error) {
          skipLine();
          errorFunc(' Error in setBlobProperties() ');
          errorFunc(util.inspect(error));
        } else  if (options.verbose) {
          skipLine(); // do not write over progress messages
          verboseFunc('MD5 hash is computed and written to the blob: ' + md5);
        }
        md5AggMd5(error);
      });
    }
  
    function onBlobCreated(error) {
      if (error) {
        errorFunc('Cannot create or copy a blob');
        errorFunc(util.inspect(error));
        callback(error);
        return;
      }
      // initiate MD5 calculation
      if (!skipMd5) {
        sillyFunc('Starting to calculate MD5 hash for input file (use -m to skip it)');
        calculateMD5(onMD5Calculated);
      }
  
      var doneThreads = 0;
      var totalBlocks = maxRequest + 1;
      var tracker = jobTracker.getJobTracker(totalBlocks);
      var wasIdle = false;
  
      numThreads = Math.min(numThreads, maxRequest + 1);
      var runningThreads = 0;
      var alreadyDone = 0; // previously uploaded blocks - to adjust speed computation and check if anything was uploaded
  
      var fd;
      var nonZeroBlockSize = Math.max(2 * 1024 * 1024, blockSize);
      assert(nonZeroBlockSize % blockSize === 0);
      var nonZeroBlocks;
  
      if (dynVHD) {
        startThreads();
      } else {
        fd = fs.openSync(fileName, 'r');
  
        verboseFunc('Detecting the empty data blocks in the local file...');
        findNonZeroBlocks(fd, nonZeroBlockSize, function(error, res) {
          if (error) {
            errorFunc('Error while trying to detect non-zero blocks');
            errorFunc(util.inspect(error));
            callback(error);
            return;
          }
          verboseFunc('Detecting the empty data blocks completed.');
          nonZeroBlocks = res;
          startThreads();
        });
      }
  
      function startThreads() {
        for(var num = 0; num < numThreads && !tracker.isDone(); ++num) {
          runningThreads++;
          next(num);
        }
      }
  
      function printStatus() {
        var percentRequested = 100 * tracker.getRequestedPart();
        var percentCompleted = 100 * tracker.getCompletedPart();
        var doneBlocks = tracker.getCompleted();
        var speed = ' ';
        var uploadedBlocks = Math.max(doneBlocks - alreadyDone, 0);
        if ((doneBlocks >= numThreads || percentCompleted > 90) && uploadedBlocks > 0) {
          var elapsedTime = (new Date() - startTime) / 1000;
          var kbps = (blockSize / 1024 * uploadedBlocks) / elapsedTime;
          var timeMinSec = (elapsedTime % 60).toFixed();
          if (elapsedTime >= 60) {
            timeMinSec = Math.floor(elapsedTime / 60) + 'm' + extendLeftTo(timeMinSec, 2);
          }
          speed = ' Time:' + extendLeftTo(timeMinSec, 5) + 's Speed:' + extendLeftTo(kbps, 6, 0) + ' KB/s';
        }
        var runningRequests = runningThreads - doneThreads;
        var string =
          'Requested:' + extendLeftTo(percentRequested, 5, 1) + '%' +
          ' Completed:' + extendLeftTo(percentCompleted, 5, 1) + '%' +
          ' Running:' + extendLeftTo(runningRequests, 4) +
          speed + ' \r';
        // Write to stdout (fd is 1)
        // console.log() cannot be used because it will not pass '\r' as it it (will do '\n')
        fs.writeSync(1, string);
      }
  
      function next(number) {
        var index = tracker.nextIndex();
        if (index === null || index === undefined) {
          wasIdle = true;
          doneThreads++;
          printStatus();
          if (doneThreads == runningThreads) {
            skipLine();
            if (md5Agg.isInProgressFor('md5')) {
              logFunc('Finishing computing MD5 hash, ' + (md5Done * 100 / size).toFixed() +'% is complete.');
            }
  
            md5AggUpload(null, alreadyDone === tracker.getCompleted());
          }
          return;
        }
  
        function toNext(error, pageBlob, response) {
          // To avoid possible deep recursion, Use setImmediate instead of invoking next() directly
          // Use process.nextTick in older versions of Node, where setImmediate is not available
          var reschedule = global.setImmediate || process.nextTick;
          
          if (exiting) {
            return; // do not display all errors
          }
          if (error) {
            if (options.verbose) {
              skipLine(); // skip progress line
              warnFunc(number + '> Error at upload index ' + index + ' - will retry. ');
            }
            // on errors, remove one 'thread' on every other error
            // except when we are done with sending requests - in which case that some 'threads' are likely already finished
            if (tracker.error(index) % 2 && (!wasIdle || tracker.getErrorRate() > 0.95)) { // always go there for now
              doneThreads++;
              if (doneThreads == numThreads) {
                printStatus();
                skipLine();
                errorFunc('Error in blobService.createBlobPagesFrom...()');
                errorFunc(util.inspect(error));
                if (response) errorFunc('response=' + util.inspect(response));
                exiting = true;
                callback(error);
                return;
              }
              printStatus();
            } else {
              reschedule(function() {next(number);});
            }
            return;
          }
          tracker.done(index);
  
          reschedule(function() {next(number);});
        }
  
        if (exiting) {
          return;
        }
        uploadAtIndex(index ? index - 1 : maxRequest); // change indices to start uploading from the footer, in both dynamic and fixed VHD cases, then continue from the beginning
        printStatus();
  
        function isAlreadyUploaded(start, end) {
          if (uploadedRegions) {
            var set = intSet.createIntSet(start, end);
            set.subtractRanges(uploadedRegions);
            return set.isEmpty();
          }
          return false;
        }
  
        function uploadAtIndex(index) {
          if (index > maxIndex) { // create a footer
            vhdInfo.footer.convertToFixed();
            if (!size || size % 512) {
              errorFunc('Incorrect file size - should be a multiple of 512: ' + size + '\n size % 512 =' + size % 512);
            }
            if (vhdInfo.footer.buffer.length !== 512) {
              errorFunc('Error: footer length (should be 512) =' + vhdInfo.footer.buffer.length);
            }
            // check if already uploaded
            if (isAlreadyUploaded(size - 512, size - 1)) {
              // nothing to do here
              alreadyDone++;
              toNext();
              return;
            }
            // don't use string object, either by using Buffer.toString() or manually - this won't work
            blobService.createPagesFromStream(container, blobName,
              vhdInfo.footer.buffer, size - 512, size - 1, toNext);
          } else {
            var posIn = index * blockSize;
            var posOut = posIn;
            if (dynVHD) {
              var batIndex = Math.floor(index / uploadBlocksPerBATBlocks);
              posIn = vhdInfo.getDynVhdOffset(posOut);
              if (vhdInfo.fullIndices) {
                posOut = vhdInfo.fullIndices[batIndex] * vhdInfo.ddHeader.blockSize + (index % uploadBlocksPerBATBlocks) * blockSize;
              }
            } else {
              // check if all zeroes (fixed vhd)
              var nonZeroBlockIndex = Math.floor(index * blockSize / nonZeroBlockSize);
              if (!nonZeroBlocks.get(nonZeroBlockIndex)) {
                // nothing to do here
                alreadyDone++;
                toNext();
                return;
              }
            }
  
            var realEndPosIn = Math.min(posIn + blockSize, size);
            realEndPosIn--;
            var realEndPosOut = Math.min(posOut + blockSize, size);
            realEndPosOut--;
  
            // check if already uploaded
            if (isAlreadyUploaded(posOut, realEndPosOut)) {
              // nothing to do here
              alreadyDone++;
              toNext();
              return;
            }
  
            var readStream;
            if (dynVHD) {
              readStream = vhdInfo.getReadStream({start: posOut, end: realEndPosOut});
            } else {
              readStream = fs.createReadStream(fileName,
                {fd: fd, autoClose: false, start: posIn, end: realEndPosIn});
            }
  
            if (!readStream || !size) {
              callback('Cannot create read stream');
              throw new Error('Cannot create read stream');
            }
  
            if (posOut % 512 !== 0) {
              errorFunc(posOut + ': posOut % 512 =' + posOut % 512);
            }
  
            blobService.createPagesFromStream(container, blobName,
              readStream, posOut, realEndPosOut, toNext);
          }
        }
      }
    }
  };
  