1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | var debug = require('debug')('duo-package');
|
7 | var Emitter = require('events').EventEmitter;
|
8 | var write = require('fs').createWriteStream;
|
9 | var read = require('fs').createReadStream;
|
10 | var resolve = require('gh-resolve');
|
11 | var mkdir = require('mkdirp').sync;
|
12 | var netrc = require('node-netrc');
|
13 | var fmt = require('util').format;
|
14 | var enstore = require('enstore');
|
15 | var tmp = require('os').tmpdir();
|
16 | var unyield = require('unyield');
|
17 | var thunk = require('thunkify');
|
18 | var semver = require('semver');
|
19 | var tar = require('tar-fs');
|
20 | var path = require('path');
|
21 | var zlib = require('zlib');
|
22 | var fs = require('co-fs');
|
23 | var url = require('url');
|
24 | var join = path.join;
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | module.exports = Package;
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | resolve = thunk(resolve);
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | var inflight = {};
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | var refs = {};
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | var home = process.env.HOME || process.env.HOMEPATH;
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 | var cachepath = join(tmp, 'duo');
|
62 | mkdir(cachepath);
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | var api = 'https://api.github.com';
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | var auth = netrc('api.github.com') || {
|
75 | password: process.env.GH_TOKEN
|
76 | };
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 | auth.password
|
83 | ? debug('read auth details from ~/.netrc')
|
84 | : debug('could not read auth details from ~/.netrc')
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | var unauthorized = 'You have not authenticated and this repo may be private.'
|
91 | + ' Make sure you have a ~/.netrc entry or specify $GH_TOKEN=<token>.';
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 | function Package(repo, ref) {
|
102 | if (!(this instanceof Package)) return new Package(repo, ref);
|
103 | this.repo = repo.replace(':', '/');
|
104 | this.tok = auth.password || null;
|
105 | this.setMaxListeners(Infinity);
|
106 | this.dir = process.cwd();
|
107 | this.ua = 'duo-package';
|
108 | this.resolved = false;
|
109 | this.ref = ref || '*';
|
110 | this._cache = true;
|
111 | Emitter.call(this);
|
112 | };
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | Package.prototype.__proto__ = Emitter.prototype;
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | Package.prototype.directory = function(dir) {
|
129 | if (!dir) return this.dir;
|
130 | this.dir = dir;
|
131 | return this;
|
132 | };
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 | Package.prototype.path = function(path) {
|
142 | return join(this.dir, this.slug(), path || '');
|
143 | };
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 | Package.prototype.useragent = function(ua) {
|
153 | if (!ua) return this.ua;
|
154 | this.ua = ua;
|
155 | return this;
|
156 | };
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | Package.prototype.token = function(token) {
|
167 | if (!arguments.length) return this.tok;
|
168 | this.tok = token;
|
169 | return this;
|
170 | };
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | Package.prototype.resolve = unyield(function *() {
|
180 |
|
181 |
|
182 | if (semver.valid(this.ref) || !semver.validRange(this.ref)) {
|
183 | this.debug('don\'t need to resolve %s', this.ref);
|
184 | this.resolved = this.ref;
|
185 | return this.resolved;
|
186 | }
|
187 |
|
188 |
|
189 | var slug = this.repo + '@' + this.ref;
|
190 | this.resolved = this.resolved || cached(this.repo, this.ref);
|
191 |
|
192 |
|
193 | if (this.resolved) {
|
194 | this.debug('resolved from cache %s', this.resolved);
|
195 | return this.resolved;
|
196 | }
|
197 |
|
198 |
|
199 | this.emit('resolving');
|
200 | this.debug('resolving');
|
201 |
|
202 |
|
203 | var ref = yield resolve(slug, { token: this.tok });
|
204 |
|
205 |
|
206 | if (!ref) throw this.error('%s: reference %s not found', this.slug(), this.ref);
|
207 |
|
208 |
|
209 | this.resolved = ref.name;
|
210 | (refs[this.repo] = refs[this.repo] || []).push(ref.name);
|
211 | this.debug('resolved');
|
212 | this.emit('resolve');
|
213 |
|
214 | return ref.name;
|
215 | });
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 | Package.prototype.fetch = unyield(function *() {
|
226 |
|
227 |
|
228 | var ref = this.resolved || (yield this.resolve());
|
229 | var token = this.tok ? this.tok + '@' : '';
|
230 | var url = fmt('https://%sgithub.com/%s/archive/%s.tar.gz', token, this.repo, ref);
|
231 | var cache = join(cachepath, this.slug() + '.tar.gz');
|
232 | var dest = this.path();
|
233 |
|
234 |
|
235 | if (inflight[dest]) {
|
236 | var pkg = inflight[dest];
|
237 | yield function(done){ pkg.once('fetch', done); }
|
238 | }
|
239 |
|
240 |
|
241 | inflight[dest] = this;
|
242 |
|
243 |
|
244 | if (yield this.exists()) {
|
245 |
|
246 |
|
247 | this.emit('fetching');
|
248 | this.debug('already exists');
|
249 | this.emit('fetch');
|
250 | delete inflight[dest];
|
251 |
|
252 | return this;
|
253 | }
|
254 |
|
255 |
|
256 | if (yield exists(cache)) {
|
257 |
|
258 |
|
259 | this.emit('fetching');
|
260 | this.emit('installing');
|
261 | this.debug('extracting from cache')
|
262 |
|
263 |
|
264 | yield this.extract(cache, dest);
|
265 |
|
266 |
|
267 | this.emit('fetch');
|
268 | this.emit('install')
|
269 | this.debug('extracted from cache')
|
270 | delete inflight[dest];
|
271 |
|
272 | return this;
|
273 | }
|
274 |
|
275 |
|
276 | this.emit('fetching');
|
277 | this.emit('installing');
|
278 | this.debug('fetching from %s', url);
|
279 |
|
280 |
|
281 | var store = yield this.download(url);
|
282 |
|
283 |
|
284 | var cacheable = this._cache && semver.validRange(ref);
|
285 | var gens = [];
|
286 |
|
287 | if (cacheable) gens.push(this.write(store, cache));
|
288 | gens.push(this.extract(store, dest));
|
289 |
|
290 | yield gens;
|
291 |
|
292 |
|
293 | this.emit('fetch');
|
294 | this.emit('install');
|
295 | this.debug('fetched from %s', url);
|
296 | delete inflight[dest];
|
297 |
|
298 | return this;
|
299 | });
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 | Package.prototype.exists = unyield(function*(){
|
309 | return yield exists(this.path());
|
310 | });
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 | Package.prototype.toString =
|
320 | Package.prototype.slug = function() {
|
321 | var repo = this.repo.replace('/', '-');
|
322 | var ref = this.resolved || this.ref;
|
323 | return repo + '@' + ref;
|
324 | };
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 | Package.prototype.options = function(url, other){
|
336 | var token = this.token;
|
337 | var user = this.user;
|
338 |
|
339 | var opts = {
|
340 | url: url,
|
341 | headers: { 'User-Agent': this.ua }
|
342 | };
|
343 |
|
344 | if (other) {
|
345 | for (var k in other) opts[k] = other[k];
|
346 | }
|
347 |
|
348 | if (token) opts.headers.Authorization = 'Bearer ' + token;
|
349 |
|
350 | return opts;
|
351 | };
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 | Package.prototype.download = function(url) {
|
363 | var store = enstore();
|
364 | var gzip = store.createWriteStream();
|
365 | var opts = { headers: {} };
|
366 | var tok = this.token();
|
367 | var self = this;
|
368 | var prev = 0;
|
369 | var len = 0;
|
370 |
|
371 |
|
372 | opts.headers['User-Agent'] = this.ua;
|
373 | opts.url = url;
|
374 |
|
375 | return function(fn) {
|
376 | var req = request(opts);
|
377 | debug(curl(opts));
|
378 |
|
379 |
|
380 | req.on('error', error);
|
381 |
|
382 | store.on('end', function() {
|
383 | return fn(null, store);
|
384 | });
|
385 |
|
386 | req.on('response', function(res) {
|
387 | var status = res.statusCode;
|
388 | var headers = res.headers;
|
389 |
|
390 |
|
391 | var total = +headers['content-length'];
|
392 |
|
393 |
|
394 | if (status < 200 || status >= 300) {
|
395 | var statusError = 406 == status && !tok
|
396 | ? self.error('returned with status code: %s. %s', status, unauthorized)
|
397 | : self.error('returned with status code: %s', status);
|
398 | return fn(statusError);
|
399 | }
|
400 |
|
401 |
|
402 | req.on('data', function(buf) {
|
403 | len += buf.length;
|
404 | var percent = Math.round(len / total * 100);
|
405 |
|
406 | if (!total || prev >= percent) return;
|
407 | self.debug('progress %s', percent);
|
408 | self.emit('progress', percent);
|
409 | prev = percent;
|
410 | });
|
411 |
|
412 |
|
413 | req.pipe(zlib.createGunzip())
|
414 | .on('error', error)
|
415 | .pipe(gzip);
|
416 | });
|
417 |
|
418 | function error(err) {
|
419 | return fn(self.error(err));
|
420 | }
|
421 | }
|
422 | };
|
423 |
|
424 |
|
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 | Package.prototype.extract = function(store, dest) {
|
434 | var self = this;
|
435 |
|
436 |
|
437 | var stream = 'string' == typeof store
|
438 | ? read(store)
|
439 | : store.createReadStream();
|
440 |
|
441 | return function(fn) {
|
442 | stream
|
443 | .on('error', error)
|
444 | .pipe(tar.extract(dest, { strip: 1 }))
|
445 | .on('error', error)
|
446 | .on('finish', fn);
|
447 |
|
448 | function error(err) {
|
449 | return fn(self.error(err));
|
450 | }
|
451 | };
|
452 | };
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 |
|
461 |
|
462 |
|
463 | Package.prototype.write = function(store, dest) {
|
464 | var read = store.createReadStream();
|
465 | var stream = write(dest);
|
466 | var self = this;
|
467 |
|
468 | return function(fn) {
|
469 | read.pipe(stream)
|
470 | .on('error', error)
|
471 | .on('finish', fn)
|
472 |
|
473 | function error(err) {
|
474 | return fn(self.error(err));
|
475 | }
|
476 | };
|
477 | };
|
478 |
|
479 |
|
480 |
|
481 |
|
482 |
|
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 | Package.prototype.debug = function(msg) {
|
489 | var args = [].slice.call(arguments, 1);
|
490 | msg = fmt('%s: %s', this.slug(), msg);
|
491 | debug.apply(debug, [msg].concat(args));
|
492 | return this;
|
493 | };
|
494 |
|
495 |
|
496 |
|
497 |
|
498 |
|
499 |
|
500 |
|
501 |
|
502 |
|
503 | Package.prototype.error = function(msg) {
|
504 | msg = fmt('%s: %s', this.slug(), msg.message || msg);
|
505 | var args = [].slice.call(arguments, 1);
|
506 | return new Error(fmt.apply(null, [msg].concat(args)));
|
507 | };
|
508 |
|
509 |
|
510 |
|
511 |
|
512 |
|
513 |
|
514 |
|
515 |
|
516 |
|
517 |
|
518 | function cached(repo, ref){
|
519 | var revs = refs[repo] || [];
|
520 |
|
521 | for (var i = 0; i < revs.length; ++i) {
|
522 | try {
|
523 | if (semver.satisfies(revs[i], ref)) return revs[i];
|
524 | } catch (e) {
|
525 | if (revs[i] == ref) return revs[i];
|
526 | }
|
527 | }
|
528 | }
|
529 |
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 | function curl(opts) {
|
539 | var arr = ['curl'];
|
540 |
|
541 |
|
542 | arr.push('-v');
|
543 | arr.push('-L');
|
544 |
|
545 |
|
546 | Object
|
547 | .keys(opts.headers)
|
548 | .forEach(function(header) {
|
549 | arr.push(fmt('-H "%s: %s"', header, opts.headers[header]));
|
550 | });
|
551 |
|
552 |
|
553 | arr.push(opts.url);
|
554 |
|
555 | return arr.join(' ');
|
556 | }
|
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 | function request() {
|
563 | var req = require('request');
|
564 | return req.apply(req, arguments);
|
565 | }
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 | function *exists(path) {
|
576 | try {
|
577 | yield fs.stat(path);
|
578 | return true;
|
579 | } catch (e) {
|
580 | return false;
|
581 | }
|
582 | }
|