UNPKG

8.85 kBJavaScriptView Raw
1/**
2 * This code is closed source and Confidential and Proprietary to
3 * Appcelerator, Inc. All Rights Reserved. This code MUST not be
4 * modified, copied or otherwise redistributed without express
5 * written permission of Appcelerator. This file is licensed as
6 * part of the Appcelerator Platform and governed under the terms
7 * of the Appcelerator license agreement.
8 */
9var ProgressBar = require('progress'),
10 chalk = require('chalk'),
11 util = require('./util'),
12 errorlib = require('./error'),
13 urllib = require('url'),
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 = 10,
20 pendingRequest;
21
22function download(quiet, force, wantVersion, tmpfile, stream, location, callback, nobanner, retryAttempts) {
23 debug('download called with arguments: %o',arguments);
24 if (!nobanner && !wantVersion) { util.waitMessage('Finding latest version ...'); }
25 if (!nobanner && wantVersion) { util.waitMessage('Finding version '+wantVersion+' ...'); }
26 retryAttempts = retryAttempts || 1;
27 var bar;
28 pendingRequest = util.request(location, function (err,res,req) {
29 if (err) {
30 debug('error from download was: %o',err);
31 if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') {
32 pendingRequest = null;
33 util.resetLine();
34 if (retryAttempts > MAX_RETRIES) {
35 return callback(errorlib.createError('com.appcelerator.install.download.server.unavailable'));
36 }
37 // retry again
38 debug('retrying request again, count=%d, delay=%d',retryAttempts,500*retryAttempts);
39 return setTimeout(function() {
40 download(quiet, force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts+1);
41 },500*retryAttempts);
42 }
43 else if (err.name === 'AppCError') {
44 return callback(err);
45 }
46 return callback(errorlib.createError('com.appcelerator.install.download.server.response.error',err.message));
47 }
48 debug('response status code was: %d',res.statusCode);
49 // console.log(res);
50 if (res.statusCode===301 || res.statusCode===302) {
51 // handle redirect
52 location = res.headers.location;
53 pendingRequest = null;
54 util.resetLine();
55 return download(quiet, force, wantVersion, tmpfile, stream, location, callback, nobanner, retryAttempts);
56 }
57 else if (res.statusCode===404) {
58 pendingRequest = null;
59 return callback(errorlib.createError('com.appcelerator.install.download.version.specified.incorrect',wantVersion));
60 }
61 else if (res.statusCode===200) {
62 debug('response headers: %j',res.headers);
63
64 var version = res.headers['x-appc-version'] || res.headers['x-amz-meta-version'],
65 shasum = res.headers['x-appc-shasum'] || res.headers['x-amz-meta-shasum'],
66 hash = require('crypto').createHash('sha1');
67
68 hash.setEncoding('hex');
69
70 debug('download version: %s, shasum: %s',version,shasum);
71
72 if (!nobanner && !wantVersion) { util.okMessage(chalk.green(version)); }
73 if (!nobanner && wantVersion) { util.okMessage(); }
74
75 // check to see if we have it already installed and if we do, just continue
76 if (!force && version) {
77 var bin = util.getInstallBinary(null, version);
78 if (bin) {
79 return callback(null, null, version, bin);
80 }
81 }
82
83 var total = parseInt(res.headers['content-length'], 10);
84 debug('download content-length: %d',total);
85
86 if (!total) {
87 return callback(errorlib.createError('com.appcelerator.install.download.invalid.content.length'));
88 }
89
90 bar = (!nobanner && process.stdout.isTTY && !process.env.TRAVIS) &&
91 new ProgressBar('Downloading [:bar] :percent :etas', {
92 complete: util.isWindows() ? '█' : chalk.green('▤'),
93 incomplete: ' ',
94 width: Math.max(40, Math.round(process.stdout.columns/2)),
95 total: total,
96 clear: true,
97 stream: process.stdout
98 });
99 var count = 0,
100 tickCount = 0,
101 tickDiff = total*0.01;
102
103 util.stopSpinner();
104
105 if (!bar) {
106 util.waitMessage('Downloading ...');
107 }
108
109 res.on('data', function (chunk) {
110 if (chunk.length) {
111 if (bar) {
112 tickCount += chunk.length;
113 if (tickCount > tickDiff) {
114 bar.tick(tickCount);
115 tickCount = 0;
116 }
117 }
118 stream.write(chunk);
119 hash.update(chunk);
120 count+=chunk.length;
121 }
122 });
123
124 res.on('error', function(err){
125 debug('download error %o',err);
126 try {
127 stream.end();
128 }
129 catch (E) {
130 }
131 pendingRequest = null;
132 callback(errorlib.createError('com.appcelerator.install.download.server.stream.error',err.message));
133 });
134
135 res.on('end', function () {
136 debug('download end');
137 if (bar) { bar.tick(tickCount); }
138 stream.end();
139 pendingRequest = null;
140 // check to make sure we downloaded all the bytes we needed too
141 // if not, this means the download failed and we should attempt to re-start it
142 if (count !== total) {
143 debug('download max retry');
144 if (bar) { bar.terminate(); util.resetLine(); }
145 stream.end();
146 if (retryAttempts >= MAX_RETRIES) {
147 return callback(errorlib.createError('com.appcelerator.install.download.failed.retries.max',retryAttempts));
148 }
149 // re-open stream
150 stream = fs.createWriteStream(tmpfile);
151 var delay = retryAttempts * 2000;
152 // download failed, we should re-start
153 return setTimeout(function(){
154 download(force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts+1);
155 },delay);
156 }
157 hash.end();
158 var checkshasum = hash.read();
159 debug('download checkshasum: %s',checkshasum);
160 // our downloaded file checksum should match what we uploaded, if not, this is a security violation
161 if (checkshasum!==shasum) {
162 return callback(errorlib.createError('com.appcelerator.install.download.failed.checksum',shasum,checkshasum));
163 }
164 else {
165 if (!quiet) {
166 util.infoMessage('Validating security checksum '+chalk.green(util.isWindows()?'OK':'✓'));
167 }
168 }
169 process.nextTick(function(){
170 callback(null, tmpfile, version);
171 });
172 });
173 }
174 else if (/^(408|500|503)$/.test(String(res.statusCode))) {
175 // some sort of error on the server, let's re-try again ...
176 // 408 is a server timeout
177 // 500 is a server error
178 // 503 is a server unavailable. this could be a deployment in progress
179 debug('download server error ... will retry');
180 stream.end();
181 if (bar) { util.resetLine(); }
182 pendingRequest = null;
183 if (retryAttempts >= MAX_RETRIES) {
184 debug('download server error ... maxed out after %d attempts',retryAttempts);
185 return callback(errorlib.createError('com.appcelerator.install.download.server.unavailable'));
186 }
187 var delay = retryAttempts * 500;
188 debug('download server error ... retry delay %d ms',delay);
189 stream = fs.createWriteStream(tmpfile);
190 return setTimeout(function() {
191 download(quiet, force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts+1);
192 },delay);
193 }
194 else {
195 debug('download server unexpected error %d',res.statusCode);
196 stream.end();
197 if (bar) { util.resetLine(); }
198 pendingRequest = null;
199 return callback(errorlib.createError('com.appcelerator.install.download.server.response.unexpected',res.statusCode));
200 }
201 });
202}
203
204exports.start = function(quiet, banner, force, location, wantVersion, callback) {
205 var tmpfile = path.join(tmpdir, 'appc-'+(+new Date())+'.tar.gz'),
206 stream = fs.createWriteStream(tmpfile),
207 exitFn,
208 sigintFn,
209 pendingAbort,
210 createCleanup = function createCleanup(name) {
211 return function(exit) {
212 if (pendingRequest) {
213 try {
214 // abort the pending HTTP request so it will
215 // close the server socket
216 pendingRequest.abort();
217 }
218 catch (E) {
219 }
220 pendingRequest = null;
221 }
222 try {
223 if (fs.existSync(tmpfile)) {
224 fs.unlinkSync(tmpfile);
225 }
226 }
227 catch (E) {
228 }
229 if (name==='SIGINT') {
230 pendingAbort = true;
231 process.removeListener('SIGINT',sigintFn);
232 util.abortMessage('Download');
233 }
234 else if (name==='exit') {
235 process.removeListener('exit',exitFn);
236 if (!pendingAbort) {
237 process.exit(exit);
238 }
239 }
240 else {
241 process.removeListener('exit',exitFn);
242 process.removeListener('SIGINT',sigintFn);
243 }
244 };
245 };
246
247 // make sure we remove the file on shutdown
248 process.on('exit', (exitFn=createCleanup('exit')));
249 process.on('SIGINT', (sigintFn=createCleanup('SIGINT')));
250
251 // default banner is on for process downloads unless quiet or no banner
252 quiet = quiet===undefined ? false : quiet;
253 banner = (banner===undefined ? true : banner) && !(quiet);
254
255 debug('download start, quiet %d, banner %d',quiet,banner);
256
257 // run the download
258 download(quiet, force, wantVersion, tmpfile, stream, location, function(){
259 // remove clean listeners
260 createCleanup('done')();
261 // carry on... 🙏
262 return callback.apply(null,arguments);
263 },!banner);
264};