Blame view

node_modules/bower/lib/util/download.js 3.93 KB
2dda2e10   Administrator   generator ignore
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
  var progress = require('request-progress');
  var request = require('request');
  var Q = require('q');
  var mout = require('mout');
  var retry = require('retry');
  var createError = require('./createError');
  var createWriteStream = require('fs-write-stream-atomic');
  var destroy = require('destroy');
  
  var errorCodes = [
      'EADDRINFO',
      'ETIMEDOUT',
      'ECONNRESET',
      'ESOCKETTIMEDOUT',
      'ENOTFOUND'
  ];
  
  function download(url, file, options) {
      var operation;
      var deferred = Q.defer();
      var progressDelay = 8000;
  
      options = mout.object.mixIn({
          retries: 5,
          factor: 2,
          minTimeout: 1000,
          maxTimeout: 35000,
          randomize: true,
          progressDelay: progressDelay,
          gzip: true
      }, options || {});
  
      // Retry on network errors
      operation = retry.operation(options);
  
      operation.attempt(function () {
          Q.fcall(fetch, url, file, options)
          .then(function (response) {
              deferred.resolve(response);
          })
          .progress(function (status) {
              deferred.notify(status);
          })
          .fail(function (error) {
              // Save timeout before retrying to report
              var timeout = operation._timeouts[0];
  
              // Reject if error is not a network error
              if (errorCodes.indexOf(error.code) === -1) {
                  return deferred.reject(error);
              }
  
              // Next attempt will start reporting download progress immediately
              progressDelay = 0;
  
              // This will schedule next retry or return false
              if (operation.retry(error)) {
                  deferred.notify({
                      retry: true,
                      delay: timeout,
                      error: error
                  });
              } else {
                  deferred.reject(error);
              }
          });
      });
  
      return deferred.promise;
  }
  
  function fetch(url, file, options) {
      var deferred = Q.defer();
  
      var contentLength;
      var bytesDownloaded = 0;
  
      var reject = function (error) {
          deferred.reject(error);
      };
  
      var req = progress(request(url, options), {
          delay: options.progressDelay
      })
      .on('response', function (response) {
          contentLength = Number(response.headers['content-length']);
  
          var status = response.statusCode;
  
          if (status < 200 || status >= 300) {
              return deferred.reject(createError('Status code of ' + status, 'EHTTP'));
          }
  
          var writeStream = createWriteStream(file);
          var errored = false;
  
          // Change error listener so it cleans up writeStream before exiting
          req.removeListener('error', reject);
          req.on('error', function (error) {
              errored = true;
              destroy(req);
              destroy(writeStream);
  
              // Wait for writeStream to cleanup after itself...
              // TODO: Maybe there's a better way?
              setTimeout(function () {
                  deferred.reject(error);
              }, 50);
          });
  
          writeStream.on('finish', function () {
              if (!errored) {
                  destroy(req);
                  deferred.resolve(response);
              }
          });
  
          req.pipe(writeStream);
      })
      .on('data', function (data) {
          bytesDownloaded += data.length;
      })
      .on('progress', function (state) {
          deferred.notify(state);
      })
      .on('error', reject)
      .on('end', function () {
          // Check if the whole file was downloaded
          // In some unstable connections the ACK/FIN packet might be sent in the
          // middle of the download
          // See: https://github.com/joyent/node/issues/6143
          if (contentLength && bytesDownloaded < contentLength) {
              req.emit('error', createError(
                  'Transfer closed with ' + (contentLength - bytesDownloaded) + ' bytes remaining to read',
                  'EINCOMPLETE'
              ));
          }
      });
  
      return deferred.promise;
  }
  
  module.exports = download;