1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | var ProgressBar = require('progress'),
|
11 | chalk = require('chalk'),
|
12 | util = require('./util'),
|
13 | errorlib = require('./error'),
|
14 | fs = require('fs'),
|
15 | os = require('os'),
|
16 | path = require('path'),
|
17 | debug = require('debug')('appc:download'),
|
18 | tmpdir = os.tmpdir(),
|
19 | MAX_RETRIES = util.MAX_RETRIES,
|
20 | pendingRequest;
|
21 |
|
22 | function download(quiet, force, wantVersion, tmpfile, stream, location, callback, nobanner, retryAttempts) {
|
23 | debug('download called with arguments: %o', arguments);
|
24 | if (!nobanner && !wantVersion) {
|
25 | util.waitMessage('Finding latest version ');
|
26 | }
|
27 | if (!nobanner && wantVersion) {
|
28 | util.waitMessage('Finding version ' + wantVersion + ' ');
|
29 | }
|
30 |
|
31 | retryAttempts = retryAttempts || 1;
|
32 | debug('connection attempt %d of %d', retryAttempts, MAX_RETRIES);
|
33 |
|
34 | var bar;
|
35 | pendingRequest = util.request(location, function (err, res, _req) {
|
36 | if (err) {
|
37 | debug('error from download was: %o', err);
|
38 | if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') {
|
39 | pendingRequest = null;
|
40 | util.resetLine();
|
41 | if (retryAttempts >= MAX_RETRIES) {
|
42 | return callback(errorlib.createError('com.appcelerator.install.download.server.unavailable'));
|
43 | }
|
44 |
|
45 | debug('retrying request again, count=%d, delay=%d', retryAttempts, 500 * retryAttempts);
|
46 | return setTimeout(function () {
|
47 | download(quiet, force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts + 1);
|
48 | }, 500 * retryAttempts);
|
49 | } else if (err.name === 'AppCError') {
|
50 | return callback(err);
|
51 | }
|
52 | return callback(errorlib.createError('com.appcelerator.install.download.server.response.error', err.message));
|
53 | }
|
54 | debug('response status code was: %d', res.statusCode);
|
55 |
|
56 | if (res.statusCode === 301 || res.statusCode === 302) {
|
57 | debug('response status code: %d with headers: %j', res.statusCode, res.headers);
|
58 |
|
59 |
|
60 | location = res.headers.location;
|
61 | pendingRequest = null;
|
62 | util.resetLine();
|
63 | return download(quiet, force, wantVersion, tmpfile, stream, location, callback, nobanner, retryAttempts);
|
64 | } else if (res.statusCode === 404) {
|
65 | debug('response status code: %d with headers: %j', res.statusCode, res.headers);
|
66 |
|
67 | pendingRequest = null;
|
68 | return callback(errorlib.createError('com.appcelerator.install.download.version.specified.incorrect', wantVersion));
|
69 | } else if (res.statusCode === 200) {
|
70 | debug('response status code: %d with headers: %j', res.statusCode, res.headers);
|
71 |
|
72 | var version = res.headers['x-appc-version'] || res.headers['x-amz-meta-version'],
|
73 | shasum = res.headers['x-appc-shasum'] || res.headers['x-amz-meta-shasum'],
|
74 | hash = require('crypto').createHash('sha1');
|
75 |
|
76 | hash.setEncoding('hex');
|
77 |
|
78 | debug('download version: %s, shasum: %s', version, shasum);
|
79 |
|
80 | if (!nobanner && !wantVersion) {
|
81 | util.okMessage(chalk.green(version));
|
82 | }
|
83 | if (!nobanner && wantVersion) {
|
84 | util.okMessage();
|
85 | }
|
86 |
|
87 |
|
88 | if (!force && version) {
|
89 | var bin = util.getInstallBinary(null, version);
|
90 | if (bin) {
|
91 | return callback(null, null, version, bin);
|
92 | }
|
93 | }
|
94 |
|
95 | var total = parseInt(res.headers['content-length'], 10);
|
96 | debug('download content-length: %d', total);
|
97 |
|
98 | if (!total) {
|
99 | return callback(errorlib.createError('com.appcelerator.install.download.invalid.content.length'));
|
100 | }
|
101 |
|
102 | bar = (!nobanner && process.stdout.isTTY && !process.env.TRAVIS)
|
103 | && new ProgressBar('Downloading [:bar] :percent :etas', {
|
104 | complete: util.isWindows() ? '█' : chalk.green('▤'),
|
105 | incomplete: ' ',
|
106 | width: Math.max(40, Math.round(process.stdout.columns / 2)),
|
107 | total: total,
|
108 | clear: true,
|
109 | stream: process.stdout
|
110 | });
|
111 | var count = 0,
|
112 | tickCount = 0,
|
113 | tickDiff = total * 0.01;
|
114 |
|
115 | util.stopSpinner();
|
116 |
|
117 | if (!bar) {
|
118 | util.waitMessage('Downloading ');
|
119 | }
|
120 |
|
121 | res.on('data', function (chunk) {
|
122 | if (chunk.length) {
|
123 | if (bar) {
|
124 | tickCount += chunk.length;
|
125 | if (tickCount > tickDiff) {
|
126 | bar.tick(tickCount);
|
127 | tickCount = 0;
|
128 | }
|
129 | }
|
130 | stream.write(chunk);
|
131 | hash.update(chunk);
|
132 | count += chunk.length;
|
133 | }
|
134 | });
|
135 |
|
136 | res.on('error', function (err) {
|
137 | debug('download error %o', err);
|
138 | try {
|
139 | stream.end();
|
140 | } catch (E) {
|
141 |
|
142 | }
|
143 | pendingRequest = null;
|
144 | callback(errorlib.createError('com.appcelerator.install.download.server.stream.error', err.message));
|
145 | });
|
146 |
|
147 | res.on('end', function () {
|
148 | debug('download end');
|
149 | if (bar) {
|
150 | bar.tick(tickCount);
|
151 | }
|
152 | stream.end();
|
153 | pendingRequest = null;
|
154 |
|
155 |
|
156 | if (count !== total) {
|
157 | debug('download max retry');
|
158 | if (bar) {
|
159 | bar.terminate();
|
160 | util.resetLine();
|
161 | }
|
162 | stream.end();
|
163 | if (retryAttempts >= MAX_RETRIES) {
|
164 | return callback(errorlib.createError('com.appcelerator.install.download.failed.retries.max', retryAttempts));
|
165 | }
|
166 |
|
167 | stream = fs.createWriteStream(tmpfile);
|
168 | var delay = retryAttempts * 2000;
|
169 |
|
170 | return setTimeout(function () {
|
171 | download(force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts + 1);
|
172 | }, delay);
|
173 | }
|
174 | hash.end();
|
175 | var checkshasum = hash.read();
|
176 | debug('download checkshasum: %s', checkshasum);
|
177 |
|
178 | if (checkshasum !== shasum) {
|
179 | return callback(errorlib.createError('com.appcelerator.install.download.failed.checksum', shasum, checkshasum));
|
180 | } else if (!quiet) {
|
181 | util.infoMessage('Validating security checksum ' + chalk.green(util.isWindows() ? 'OK' : '✓'));
|
182 | }
|
183 | process.nextTick(function () {
|
184 | callback(null, tmpfile, version);
|
185 | });
|
186 | });
|
187 | } else if (/^(408|500|503)$/.test(String(res.statusCode))) {
|
188 |
|
189 |
|
190 |
|
191 |
|
192 | debug('download server error ... will retry');
|
193 | stream.end();
|
194 | if (bar) {
|
195 | util.resetLine();
|
196 | }
|
197 | pendingRequest = null;
|
198 | if (retryAttempts >= MAX_RETRIES) {
|
199 | debug('download server error ... maxed out after %d attempts', retryAttempts);
|
200 | return callback(errorlib.createError('com.appcelerator.install.download.server.unavailable'));
|
201 | }
|
202 | var delay = retryAttempts * 500;
|
203 | debug('download server error ... retry delay %d ms', delay);
|
204 | stream = fs.createWriteStream(tmpfile);
|
205 | return setTimeout(function () {
|
206 | download(quiet, force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts + 1);
|
207 | }, delay);
|
208 | } else {
|
209 | debug('download server unexpected error %d', res.statusCode);
|
210 | stream.end();
|
211 | if (bar) {
|
212 | util.resetLine();
|
213 | }
|
214 | pendingRequest = null;
|
215 | return callback(errorlib.createError('com.appcelerator.install.download.server.response.unexpected', res.statusCode));
|
216 | }
|
217 | });
|
218 | }
|
219 |
|
220 | exports.start = function (quiet, banner, force, location, wantVersion, callback) {
|
221 | var tmpfile = path.join(tmpdir, 'appc-' + (+new Date()) + '.tar.gz'),
|
222 | stream = fs.createWriteStream(tmpfile),
|
223 | exitFn,
|
224 | sigintFn,
|
225 | pendingAbort;
|
226 | function createCleanup(name) {
|
227 | return function (exit) {
|
228 | if (pendingRequest) {
|
229 | try {
|
230 |
|
231 |
|
232 | pendingRequest.abort();
|
233 | } catch (E) {
|
234 |
|
235 | }
|
236 | pendingRequest = null;
|
237 | }
|
238 | try {
|
239 | if (fs.existSync(tmpfile)) {
|
240 | fs.unlinkSync(tmpfile);
|
241 | }
|
242 | } catch (E) {
|
243 |
|
244 | }
|
245 | if (name === 'SIGINT') {
|
246 | pendingAbort = true;
|
247 | process.removeListener('SIGINT', sigintFn);
|
248 | util.abortMessage('Download');
|
249 | } else if (name === 'exit') {
|
250 | process.removeListener('exit', exitFn);
|
251 | if (!pendingAbort) {
|
252 | process.exit(exit);
|
253 | }
|
254 | } else {
|
255 | process.removeListener('exit', exitFn);
|
256 | process.removeListener('SIGINT', sigintFn);
|
257 | }
|
258 | };
|
259 | }
|
260 |
|
261 |
|
262 | process.on('exit', (exitFn = createCleanup('exit')));
|
263 | process.on('SIGINT', (sigintFn = createCleanup('SIGINT')));
|
264 |
|
265 |
|
266 | quiet = quiet === undefined ? false : quiet;
|
267 | banner = (banner === undefined ? true : banner) && !(quiet);
|
268 |
|
269 | debug('download start, quiet %d, banner %d', quiet, banner);
|
270 |
|
271 |
|
272 | download(quiet, force, wantVersion, tmpfile, stream, location, function () {
|
273 |
|
274 | createCleanup('done')();
|
275 |
|
276 | return callback.apply(null, arguments);
|
277 | }, !banner);
|
278 | };
|