1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | var 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 |
|
22 | function 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 |
|
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 |
|
50 | if (res.statusCode===301 || res.statusCode===302) {
|
51 |
|
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 |
|
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 |
|
141 |
|
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 |
|
150 | stream = fs.createWriteStream(tmpfile);
|
151 | var delay = retryAttempts * 2000;
|
152 |
|
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 |
|
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 |
|
176 |
|
177 |
|
178 |
|
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 |
|
204 | exports.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 |
|
215 |
|
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 |
|
248 | process.on('exit', (exitFn=createCleanup('exit')));
|
249 | process.on('SIGINT', (sigintFn=createCleanup('SIGINT')));
|
250 |
|
251 |
|
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 |
|
258 | download(quiet, force, wantVersion, tmpfile, stream, location, function(){
|
259 |
|
260 | createCleanup('done')();
|
261 |
|
262 | return callback.apply(null,arguments);
|
263 | },!banner);
|
264 | };
|