@lynker-desktop/electron-sdk 0.0.9-alpha.89 → 0.0.9-alpha.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1287 @@
1
+ const fs = require('fs');
2
+ const url = require('url');
3
+ const path = require('path');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const EventEmitter = require('events');
7
+
8
+ function _interopNamespaceDefault(e) {
9
+ const n = Object.create(null);
10
+ if (e) {
11
+ for (const k in e) {
12
+ n[k] = e[k];
13
+ }
14
+ }
15
+ n.default = e;
16
+ return n;
17
+ }
18
+
19
+ const fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
20
+ const path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
21
+ const http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
22
+ const https__namespace = /*#__PURE__*/_interopNamespaceDefault(https);
23
+
24
+ const DH_STATES = {
25
+ IDLE: 'IDLE',
26
+ SKIPPED: 'SKIPPED',
27
+ STARTED: 'STARTED',
28
+ DOWNLOADING: 'DOWNLOADING',
29
+ RETRY: 'RETRY',
30
+ PAUSED: 'PAUSED',
31
+ RESUMED: 'RESUMED',
32
+ STOPPED: 'STOPPED',
33
+ FINISHED: 'FINISHED',
34
+ FAILED: 'FAILED'
35
+ };
36
+
37
+ class DownloaderHelper extends EventEmitter.EventEmitter {
38
+
39
+ /**
40
+ * Creates an instance of DownloaderHelper.
41
+ * @param {String} url
42
+ * @param {String} destFolder
43
+ * @param {Object} [options={}]
44
+ * @memberof DownloaderHelper
45
+ */
46
+ constructor(url, destFolder, options = {}) {
47
+ super({ captureRejections: true });
48
+
49
+ if (!this.__validate(url, destFolder)) {
50
+ return;
51
+ }
52
+
53
+ this.url = this.requestURL = url.trim();
54
+ this.state = DH_STATES.IDLE;
55
+ this.__defaultOpts = {
56
+ body: null,
57
+ retry: false, // { maxRetries: 3, delay: 3000 }
58
+ method: 'GET',
59
+ headers: {},
60
+ fileName: '',
61
+ timeout: -1, // -1 use default
62
+ metadata: null,
63
+ override: false, // { skip: false, skipSmaller: false }
64
+ forceResume: false,
65
+ removeOnStop: true,
66
+ removeOnFail: true,
67
+ progressThrottle: 1000,
68
+ httpRequestOptions: {},
69
+ httpsRequestOptions: {},
70
+ resumeOnIncomplete: true,
71
+ resumeIfFileExists: false,
72
+ resumeOnIncompleteMaxRetry: 5,
73
+ };
74
+ this.__opts = Object.assign({}, this.__defaultOpts);
75
+ this.__pipes = [];
76
+ this.__total = 0;
77
+ this.__downloaded = 0;
78
+ this.__progress = 0;
79
+ this.__retryCount = 0;
80
+ this.__retryTimeout = null;
81
+ this.__resumeRetryCount = 0;
82
+ this.__states = DH_STATES;
83
+ this.__promise = null;
84
+ this.__request = null;
85
+ this.__response = null;
86
+ this.__isAborted = false;
87
+ this.__isResumed = false;
88
+ this.__isResumable = false;
89
+ this.__isRedirected = false;
90
+ this.__destFolder = destFolder;
91
+ this.__statsEstimate = {
92
+ time: 0,
93
+ bytes: 0,
94
+ prevBytes: 0,
95
+ throttleTime: 0,
96
+ };
97
+ this.__fileName = '';
98
+ this.__filePath = '';
99
+ this.updateOptions(options);
100
+ }
101
+
102
+ /**
103
+ *
104
+ *
105
+ * @returns {Promise<boolean>}
106
+ * @memberof DownloaderHelper
107
+ */
108
+ start() {
109
+ const startPromise = () => new Promise((resolve, reject) => {
110
+ this.__promise = { resolve, reject };
111
+ this.__start();
112
+ });
113
+
114
+ // this will determine the file path from the headers
115
+ // and attempt to get the file size and resume if possible
116
+ if (this.__opts.resumeIfFileExists && this.state !== this.__states.RESUMED) {
117
+ return this.getTotalSize().then(({ name, total }) => {
118
+ const override = this.__opts.override;
119
+ this.__opts.override = true;
120
+ this.__filePath = this.__getFilePath(name);
121
+ this.__opts.override = override;
122
+ if (this.__filePath && fs__namespace.existsSync(this.__filePath)) {
123
+ const fileSize = this.__getFilesizeInBytes(this.__filePath);
124
+ return fileSize !== total
125
+ ? this.resumeFromFile(this.__filePath, { total, fileName: name })
126
+ : startPromise();
127
+ }
128
+ return startPromise();
129
+ });
130
+ }
131
+ return startPromise();
132
+ }
133
+
134
+ /**
135
+ *
136
+ *
137
+ * @returns {Promise<boolean>}
138
+ * @memberof DownloaderHelper
139
+ */
140
+ pause() {
141
+ if (this.state === this.__states.STOPPED) {
142
+ return Promise.resolve(true);
143
+ }
144
+
145
+ if (this.__response) {
146
+ this.__response.unpipe();
147
+ this.__pipes.forEach(pipe => pipe.stream.unpipe());
148
+ }
149
+
150
+ if (this.__fileStream) {
151
+ this.__fileStream.removeAllListeners();
152
+ }
153
+
154
+ this.__requestAbort();
155
+
156
+ return this.__closeFileStream().then(() => {
157
+ this.__setState(this.__states.PAUSED);
158
+ this.emit('pause');
159
+ return true;
160
+ });
161
+ }
162
+
163
+ /**
164
+ *
165
+ *
166
+ * @returns {Promise<boolean>}
167
+ * @memberof DownloaderHelper
168
+ */
169
+ resume() {
170
+ // if the promise is null, the download was started using resume instead of start
171
+ if (!this.__promise) {
172
+ return this.start();
173
+ }
174
+
175
+ if (this.state === this.__states.STOPPED) {
176
+ return Promise.resolve(false);
177
+ }
178
+
179
+ this.__setState(this.__states.RESUMED);
180
+ if (this.__isResumable) {
181
+ this.__isResumed = true;
182
+ this.__reqOptions['headers']['range'] = `bytes=${this.__downloaded}-`;
183
+ }
184
+ this.emit('resume', this.__isResumed);
185
+ return this.__start();
186
+ }
187
+
188
+ /**
189
+ *
190
+ *
191
+ * @returns {Promise<boolean>}
192
+ * @memberof DownloaderHelper
193
+ */
194
+ stop() {
195
+ if (this.state === this.__states.STOPPED) {
196
+ return Promise.resolve(true);
197
+ }
198
+ const removeFile = () => new Promise((resolve, reject) => {
199
+ fs__namespace.access(this.__filePath, _accessErr => {
200
+ // if can't access, probably is not created yet
201
+ if (_accessErr) {
202
+ this.__emitStop();
203
+ return resolve(true);
204
+ }
205
+
206
+ fs__namespace.unlink(this.__filePath, _err => {
207
+ if (_err) {
208
+ this.__setState(this.__states.FAILED);
209
+ this.emit('error', _err);
210
+ return reject(_err);
211
+ }
212
+ this.__emitStop();
213
+ resolve(true);
214
+ });
215
+ });
216
+ });
217
+
218
+ this.__requestAbort();
219
+
220
+ return this.__closeFileStream().then(() => {
221
+ if (this.__opts.removeOnStop) {
222
+ return removeFile();
223
+ }
224
+ this.__emitStop();
225
+ return Promise.resolve(true);
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Add pipes to the pipe list that will be applied later when the download starts
231
+ * @url https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
232
+ * @param {stream.Readable} stream https://nodejs.org/api/stream.html#stream_class_stream_readable
233
+ * @param {Object} [options=null]
234
+ * @returns {stream.Readable}
235
+ * @memberof DownloaderHelper
236
+ */
237
+ pipe(stream, options = null) {
238
+ this.__pipes.push({ stream, options });
239
+ return stream;
240
+ }
241
+
242
+ /**
243
+ * Unpipe an stream , if a stream is not specified, then all pipes are detached.
244
+ *
245
+ * @url https://nodejs.org/api/stream.html#stream_readable_unpipe_destination
246
+ * @param {stream.Readable} [stream=null]
247
+ * @returns
248
+ * @memberof DownloaderHelper
249
+ */
250
+ unpipe(stream = null) {
251
+ const unpipeStream = _stream => (this.__response)
252
+ ? this.__response.unpipe(_stream)
253
+ : _stream.unpipe();
254
+
255
+
256
+ if (stream) {
257
+ const pipe = this.__pipes.find(p => p.stream === stream);
258
+ if (pipe) {
259
+ unpipeStream(stream);
260
+ this.__pipes = this.__pipes.filter(p => p.stream !== stream);
261
+ }
262
+ return;
263
+ }
264
+
265
+ this.__pipes.forEach(p => unpipeStream(p.stream));
266
+ this.__pipes = [];
267
+ }
268
+
269
+ /**
270
+ * Where the download will be saved
271
+ *
272
+ * @returns {String}
273
+ * @memberof DownloaderHelper
274
+ */
275
+ getDownloadPath() {
276
+ return this.__filePath;
277
+ }
278
+
279
+ /**
280
+ * Indicates if the download can be resumable (available after the start phase)
281
+ *
282
+ * @returns {Boolean}
283
+ * @memberof DownloaderHelper
284
+ */
285
+ isResumable() {
286
+ return this.__isResumable;
287
+ }
288
+
289
+ /**
290
+ * Updates the options, can be use on pause/resume events
291
+ *
292
+ * @param {Object} [options={}]
293
+ * @param {String} [url='']
294
+ * @memberof DownloaderHelper
295
+ */
296
+ updateOptions(options, url = '') {
297
+ this.__opts = Object.assign({}, this.__opts, options);
298
+ this.__headers = this.__opts.headers;
299
+
300
+ if (this.__opts.timeout > -1) {
301
+ this.__opts.httpRequestOptions.timeout = this.__opts.timeout;
302
+ this.__opts.httpsRequestOptions.timeout = this.__opts.timeout;
303
+ }
304
+
305
+ // validate the progressThrottle, if invalid, use the default
306
+ if (typeof this.__opts.progressThrottle !== 'number' || this.__opts.progressThrottle < 0) {
307
+ this.__opts.progressThrottle = this.__defaultOpts.progressThrottle;
308
+ }
309
+
310
+ this.url = url || this.url;
311
+ this.__reqOptions = this.__getReqOptions(this.__opts.method, this.url, this.__opts.headers);
312
+ this.__initProtocol(this.url);
313
+ }
314
+
315
+ /**
316
+ *
317
+ * @returns {Object}
318
+ */
319
+ getOptions() {
320
+ return this.__opts;
321
+ }
322
+
323
+ /**
324
+ *
325
+ * @returns {Object| null}
326
+ */
327
+ getMetadata() {
328
+ return this.__opts.metadata;
329
+ }
330
+
331
+ /**
332
+ * Current download progress stats
333
+ *
334
+ * @returns {Stats}
335
+ * @memberof DownloaderHelper
336
+ */
337
+ getStats() {
338
+ return {
339
+ total: this.__total,
340
+ name: this.__fileName,
341
+ downloaded: this.__downloaded,
342
+ progress: this.__progress,
343
+ speed: this.__statsEstimate.bytes
344
+ };
345
+ }
346
+
347
+ /**
348
+ * Gets the total file size from the server
349
+ *
350
+ * @returns {Promise<{name:string, total:number|null}>}
351
+ * @memberof DownloaderHelper
352
+ */
353
+ getTotalSize() {
354
+ return new Promise((resolve, reject) => {
355
+ const getReqOptions = (url) => {
356
+ this.__initProtocol(url);
357
+ const headers = Object.assign({}, this.__headers);
358
+ if (headers.hasOwnProperty('range')) {
359
+ delete headers['range'];
360
+ }
361
+ const reqOptions = this.__getReqOptions('HEAD', url, headers);
362
+ return Object.assign({}, this.__reqOptions, reqOptions);
363
+ };
364
+
365
+ let retryCount = 0;
366
+ let retryTimeout = null;
367
+
368
+ const retry = (err, url) => {
369
+ if (!this.__opts.retry || typeof this.__opts.retry !== 'object') {
370
+ return Promise.reject(err || new Error('wrong retry options'));
371
+ }
372
+
373
+ if (retryTimeout) {
374
+ clearTimeout(retryTimeout);
375
+ retryTimeout = null;
376
+ }
377
+
378
+ const { delay: retryDelay = 0, maxRetries = 999 } = this.__opts.retry;
379
+
380
+ if (retryCount >= maxRetries) {
381
+ return Promise.reject(err || new Error('reached the maximum retries'));
382
+ }
383
+
384
+ retryCount++;
385
+ this.__setState(this.__states.RETRY);
386
+ this.emit('retry', retryCount, this.__opts.retry, err);
387
+
388
+ return new Promise((resolveRetry) => {
389
+ retryTimeout = setTimeout(() => {
390
+ this.__setState(this.__states.IDLE);
391
+ getRequest(url, getReqOptions(url));
392
+ resolveRetry();
393
+ }, retryDelay);
394
+ });
395
+ };
396
+
397
+ const getRequest = (url$1, options) => {
398
+ if (retryTimeout) {
399
+ clearTimeout(retryTimeout);
400
+ retryTimeout = null;
401
+ }
402
+ const req = this.__protocol.request(options, response => {
403
+ if (this.__isRequireRedirect(response)) {
404
+ const redirectedURL = /^https?:\/\//.test(response.headers.location)
405
+ ? response.headers.location
406
+ : new url.URL(response.headers.location, url$1).href;
407
+ this.emit('redirected', redirectedURL, url$1);
408
+ return getRequest(redirectedURL, getReqOptions(redirectedURL));
409
+ }
410
+ if (response.statusCode < 200 || response.statusCode >= 400) {
411
+ const err = new Error(`Response status was ${response.statusCode}`);
412
+ // 5xx server errors
413
+ if (this.__opts.retry &&
414
+ response.statusCode >= 500 && response.statusCode < 600) {
415
+ return retry(err, url$1).catch(reject);
416
+ }
417
+ return reject(err);
418
+ }
419
+ resolve({
420
+ name: this.__getFileNameFromHeaders(response.headers, response),
421
+ total: parseInt(response.headers['content-length']) || null
422
+ });
423
+ });
424
+
425
+ req.on('error', (err) => {
426
+ if (this.__opts.retry) {
427
+ return retry(err, url$1).catch(reject);
428
+ }
429
+ reject(err);
430
+ });
431
+
432
+ req.on('timeout', () => {
433
+ if (this.__opts.retry) {
434
+ return retry(new Error('timeout'), url$1).catch(reject);
435
+ }
436
+ reject(new Error('timeout'));
437
+ });
438
+
439
+ req.on('uncaughtException', (err) => {
440
+ if (this.__opts.retry) {
441
+ return retry(err, url$1).catch(reject);
442
+ }
443
+ reject(err);
444
+ });
445
+
446
+ req.end();
447
+ };
448
+ getRequest(this.url, getReqOptions(this.url));
449
+ });
450
+ }
451
+
452
+ /**
453
+ * Get the state required to resume the download after restart. This state
454
+ * can be passed back to `resumeFromFile()` to resume a download
455
+ *
456
+ * @returns {Object} Returns the state required to resume
457
+ * @memberof DownloaderHelper
458
+ */
459
+ getResumeState() {
460
+ return {
461
+ downloaded: this.__downloaded,
462
+ filePath: this.__filePath,
463
+ fileName: this.__fileName,
464
+ total: this.__total,
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Resume the download from a previous file path
470
+ *
471
+ * @param {string} filePath - The path to the file to resume from ex: C:\Users\{user}\Downloads\file.txt
472
+ * @param {Object} state - (optionl) resume download state, if not provided it will try to fetch from the headers and filePath
473
+ *
474
+ * @returns {Promise<boolean>} - Returns the same result as `start()`
475
+ * @memberof DownloaderHelper
476
+ */
477
+ resumeFromFile(filePath, state = {}) {
478
+ this.__opts.override = true;
479
+ this.__filePath = filePath;
480
+ return ((state.total && state.fileName)
481
+ ? Promise.resolve({ name: state.fileName, total: state.total })
482
+ : this.getTotalSize())
483
+ .then(({ name, total }) => {
484
+ this.__total = state.total || total;
485
+ this.__fileName = state.fileName || name;
486
+ this.__downloaded = state.downloaded || this.__getFilesizeInBytes(this.__filePath);
487
+ this.__reqOptions['headers']['range'] = `bytes=${this.__downloaded}-`;
488
+ this.__isResumed = true;
489
+ this.__isResumable = true;
490
+ this.__setState(this.__states.RESUMED);
491
+ this.emit('resume', this.__isResumed);
492
+ return new Promise((resolve, reject) => {
493
+ this.__promise = { resolve, reject };
494
+ this.__start();
495
+ });
496
+ });
497
+ }
498
+
499
+ __start() {
500
+ if (!this.__isRedirected &&
501
+ this.state !== this.__states.RESUMED) {
502
+ this.emit('start');
503
+ this.__setState(this.__states.STARTED);
504
+ this.__initProtocol(this.url);
505
+ }
506
+
507
+ // Start the Download
508
+ this.__response = null;
509
+ this.__isAborted = false;
510
+
511
+ if (this.__request && !this.__request.destroyed) {
512
+ this.__request.destroy();
513
+ }
514
+
515
+ if (this.__retryTimeout) {
516
+ clearTimeout(this.__retryTimeout);
517
+ this.__retryTimeout = null;
518
+ }
519
+
520
+ this.__request = this.__downloadRequest(this.__promise.resolve, this.__promise.reject);
521
+
522
+ // Error Handling
523
+ this.__request.on('error', this.__onError(this.__promise.resolve, this.__promise.reject));
524
+ this.__request.on('timeout', this.__onTimeout(this.__promise.resolve, this.__promise.reject));
525
+ this.__request.on('uncaughtException', this.__onError(this.__promise.resolve, this.__promise.reject, true));
526
+
527
+ if (this.__opts.body) {
528
+ this.__request.write(this.__opts.body);
529
+ }
530
+
531
+ this.__request.end();
532
+ }
533
+
534
+ /**
535
+ * Resolve pending promises from Start method
536
+ *
537
+ * @memberof DownloaderHelper
538
+ */
539
+ __resolvePending() {
540
+ if (!this.__promise) {
541
+ return;
542
+ }
543
+ const { resolve } = this.__promise;
544
+ this.__promise = null;
545
+ return resolve(true);
546
+ }
547
+
548
+ /**
549
+ *
550
+ *
551
+ * @param {Promise.resolve} resolve
552
+ * @param {Promise.reject} reject
553
+ * @returns {http.ClientRequest}
554
+ * @memberof DownloaderHelper
555
+ */
556
+ __downloadRequest(resolve, reject) {
557
+ return this.__protocol.request(this.__reqOptions, response => {
558
+ this.__response = response;
559
+
560
+ //Stats
561
+ if (!this.__isResumed) {
562
+ this.__total = parseInt(response.headers['content-length']) || null;
563
+ this.__resetStats();
564
+ }
565
+
566
+ // Handle Redirects
567
+ if (this.__isRequireRedirect(response)) {
568
+ const redirectedURL = /^https?:\/\//.test(response.headers.location)
569
+ ? response.headers.location
570
+ : new url.URL(response.headers.location, this.url).href;
571
+ this.__isRedirected = true;
572
+ this.__initProtocol(redirectedURL);
573
+ this.emit('redirected', redirectedURL, this.url);
574
+ return this.__start();
575
+ }
576
+
577
+ // check if response wans't a success
578
+ if (response.statusCode < 200 || response.statusCode >= 400) {
579
+ const err = new Error(`Response status was ${response.statusCode}`);
580
+ err.status = response.statusCode || 0;
581
+ err.body = response.body || '';
582
+
583
+ // 5xx server errors
584
+ if (response.statusCode >= 500 && response.statusCode < 600) {
585
+ return this.__onError(resolve, reject)(err);
586
+ }
587
+ this.__setState(this.__states.FAILED);
588
+ this.emit('error', err);
589
+ return reject(err);
590
+ }
591
+
592
+ if (this.__opts.forceResume) {
593
+ this.__isResumable = true;
594
+ } else if (response.headers.hasOwnProperty('accept-ranges') &&
595
+ response.headers['accept-ranges'] !== 'none') {
596
+ this.__isResumable = true;
597
+ }
598
+
599
+ this.__startDownload(response, resolve, reject);
600
+ });
601
+ }
602
+
603
+ /**
604
+ *
605
+ *
606
+ * @param {http.IncomingMessage} response
607
+ * @param {Promise.resolve} resolve
608
+ * @param {Promise.reject} reject
609
+ * @memberof DownloaderHelper
610
+ */
611
+ __startDownload(response, resolve, reject) {
612
+ let readable = response;
613
+
614
+ if (!this.__isResumed) {
615
+ const _fileName = this.__getFileNameFromHeaders(response.headers);
616
+ this.__filePath = this.__getFilePath(_fileName);
617
+ this.__fileName = this.__filePath.split(path__namespace.sep).pop();
618
+ if (fs__namespace.existsSync(this.__filePath)) {
619
+ const downloadedSize = this.__getFilesizeInBytes(this.__filePath);
620
+ const totalSize = this.__total ? this.__total : 0;
621
+ if (typeof this.__opts.override === 'object' &&
622
+ this.__opts.override.skip && (
623
+ this.__opts.override.skipSmaller ||
624
+ downloadedSize >= totalSize)) {
625
+ this.emit('skip', {
626
+ totalSize: this.__total,
627
+ fileName: this.__fileName,
628
+ filePath: this.__filePath,
629
+ downloadedSize: downloadedSize
630
+ });
631
+ this.__setState(this.__states.SKIPPED);
632
+ return resolve(true);
633
+ }
634
+ }
635
+ this.__fileStream = fs__namespace.createWriteStream(this.__filePath, {});
636
+ } else {
637
+ this.__fileStream = fs__namespace.createWriteStream(this.__filePath, { 'flags': 'a' });
638
+ }
639
+
640
+ // Start Downloading
641
+ this.emit('download', {
642
+ fileName: this.__fileName,
643
+ filePath: this.__filePath,
644
+ totalSize: this.__total,
645
+ isResumed: this.__isResumed,
646
+ downloadedSize: this.__downloaded
647
+ });
648
+ this.__retryCount = 0;
649
+ this.__isResumed = false;
650
+ this.__isRedirected = false;
651
+ this.__setState(this.__states.DOWNLOADING);
652
+ this.__statsEstimate.time = new Date();
653
+ this.__statsEstimate.throttleTime = new Date();
654
+
655
+ // Add externals pipe
656
+ readable.on('data', chunk => this.__calculateStats(chunk.length));
657
+ this.__pipes.forEach(pipe => {
658
+ readable.pipe(pipe.stream, pipe.options);
659
+ readable = pipe.stream;
660
+ });
661
+ readable.pipe(this.__fileStream);
662
+ readable.on('error', this.__onError(resolve, reject));
663
+
664
+ this.__fileStream.on('finish', this.__onFinished(resolve, reject));
665
+ this.__fileStream.on('error', this.__onError(resolve, reject));
666
+ }
667
+
668
+
669
+ /**
670
+ *
671
+ *
672
+ * @returns
673
+ * @memberof DownloaderHelper
674
+ */
675
+ __hasFinished() {
676
+ return !this.__isAborted && [
677
+ this.__states.PAUSED,
678
+ this.__states.STOPPED,
679
+ this.__states.RETRY,
680
+ this.__states.FAILED,
681
+ this.__states.RESUMED
682
+ ].indexOf(this.state) === -1;
683
+ }
684
+
685
+
686
+ /**
687
+ *
688
+ *
689
+ * @param {IncomingMessage} response
690
+ * @returns {Boolean}
691
+ * @memberof DownloaderHelper
692
+ */
693
+ __isRequireRedirect(response) {
694
+ return (response.statusCode > 300 &&
695
+ response.statusCode < 400 &&
696
+ response.headers.hasOwnProperty('location') &&
697
+ response.headers.location);
698
+ }
699
+
700
+ /**
701
+ *
702
+ *
703
+ * @param {Promise.resolve} resolve
704
+ * @param {Promise.reject} reject
705
+ * @returns {Function}
706
+ * @memberof DownloaderHelper
707
+ */
708
+ __onFinished(resolve, reject) {
709
+ return () => {
710
+ this.__fileStream.close(_err => {
711
+ if (_err) {
712
+ return reject(_err);
713
+ }
714
+ if (this.__hasFinished()) {
715
+ const isIncomplete = !this.__total ? false : this.__downloaded !== this.__total;
716
+
717
+ if (isIncomplete && this.__isResumable && this.__opts.resumeOnIncomplete &&
718
+ this.__resumeRetryCount <= this.__opts.resumeOnIncompleteMaxRetry) {
719
+ this.__resumeRetryCount++;
720
+ this.emit('warning', new Error('uncomplete download, retrying'));
721
+ return this.resume();
722
+ }
723
+
724
+ this.__setState(this.__states.FINISHED);
725
+ this.__pipes = [];
726
+ this.emit('end', {
727
+ fileName: this.__fileName,
728
+ filePath: this.__filePath,
729
+ totalSize: this.__total,
730
+ incomplete: isIncomplete,
731
+ onDiskSize: this.__getFilesizeInBytes(this.__filePath),
732
+ downloadedSize: this.__downloaded,
733
+ });
734
+ }
735
+ return resolve(this.__downloaded === this.__total);
736
+ });
737
+ };
738
+ }
739
+
740
+ /**
741
+ *
742
+ *
743
+ * @returns
744
+ * @memberof DownloaderHelper
745
+ */
746
+ __closeFileStream() {
747
+ if (!this.__fileStream) {
748
+ return Promise.resolve(true);
749
+ }
750
+ return new Promise((resolve, reject) => {
751
+ this.__fileStream.close(err => {
752
+ if (err) {
753
+ return reject(err);
754
+ }
755
+ return resolve(true);
756
+ });
757
+ });
758
+ }
759
+
760
+ /**
761
+ *
762
+ * @param {Promise.resolve} resolve
763
+ * @param {Promise.reject} reject
764
+ * @param {boolean} abortReq
765
+ * @returns {Function}
766
+ * @memberof DownloaderHelper
767
+ */
768
+ __onError(resolve, reject, abortReq = false) {
769
+ return err => {
770
+ this.__pipes = [];
771
+
772
+ if (abortReq) {
773
+ this.__requestAbort();
774
+ }
775
+
776
+ if (this.state === this.__states.STOPPED ||
777
+ this.state === this.__states.FAILED) {
778
+ return;
779
+ }
780
+ if (!this.__opts.retry) {
781
+ return this.__removeFile().finally(() => {
782
+ this.__setState(this.__states.FAILED);
783
+ this.emit('error', err);
784
+ reject(err);
785
+ });
786
+ }
787
+ return this.__retry(err)
788
+ .catch(_err => {
789
+ this.__removeFile().finally(() => {
790
+ this.__setState(this.__states.FAILED);
791
+ this.emit('error', _err ? _err : err);
792
+ reject(_err ? _err : err);
793
+ });
794
+ });
795
+ };
796
+ }
797
+
798
+ /**
799
+ *
800
+ *
801
+ * @returns {Promise<boolean>}
802
+ * @memberof DownloaderHelper
803
+ */
804
+ __retry(err = null) {
805
+ if (!this.__opts.retry || typeof this.__opts.retry !== 'object') {
806
+ return Promise.reject(err || new Error('wrong retry options'));
807
+ }
808
+
809
+ const { delay: retryDelay = 0, maxRetries = 999 } = this.__opts.retry;
810
+
811
+ // reached the maximum retries
812
+ if (this.__retryCount >= maxRetries) {
813
+ return Promise.reject(err || new Error('reached the maximum retries'));
814
+ }
815
+
816
+ this.__retryCount++;
817
+ this.__setState(this.__states.RETRY);
818
+ this.emit('retry', this.__retryCount, this.__opts.retry, err);
819
+
820
+ if (this.__response) {
821
+ this.__response.unpipe();
822
+ this.__pipes.forEach(pipe => pipe.stream.unpipe());
823
+ }
824
+
825
+ if (this.__fileStream) {
826
+ this.__fileStream.removeAllListeners();
827
+ }
828
+
829
+ this.__requestAbort();
830
+
831
+ return this.__closeFileStream().then(() =>
832
+ new Promise((resolve) =>
833
+ this.__retryTimeout = setTimeout(
834
+ () => resolve(this.__downloaded > 0 ?
835
+ this.resume() :
836
+ this.__start()),
837
+ retryDelay
838
+ )
839
+ )
840
+ );
841
+ }
842
+
843
+ /**
844
+ *
845
+ * @param {Promise.resolve} resolve
846
+ * @param {Promise.reject} reject
847
+ * @returns {Function}
848
+ * @memberof DownloaderHelper
849
+ */
850
+ __onTimeout(resolve, reject) {
851
+ return () => {
852
+ this.__requestAbort();
853
+
854
+ if (!this.__opts.retry) {
855
+ return this.__removeFile().finally(() => {
856
+ this.__setState(this.__states.FAILED);
857
+ this.emit('timeout');
858
+ reject(new Error('timeout'));
859
+ });
860
+ }
861
+
862
+ return this.__retry(new Error('timeout'))
863
+ .catch(_err => {
864
+ this.__removeFile().finally(() => {
865
+ this.__setState(this.__states.FAILED);
866
+ if (_err) {
867
+ reject(_err);
868
+ } else {
869
+ this.emit('timeout');
870
+ reject(new Error('timeout'));
871
+ }
872
+ });
873
+ });
874
+ };
875
+ }
876
+
877
+ /**
878
+ *
879
+ *
880
+ * @memberof DownloaderHelper
881
+ */
882
+ __resetStats() {
883
+ this.__retryCount = 0;
884
+ this.__downloaded = 0;
885
+ this.__progress = 0;
886
+ this.__resumeRetryCount = 0;
887
+ this.__statsEstimate = {
888
+ time: 0,
889
+ bytes: 0,
890
+ prevBytes: 0,
891
+ throttleTime: 0,
892
+ };
893
+ }
894
+
895
+ /**
896
+ *
897
+ *
898
+ * @param {Object} headers
899
+ * @returns {String}
900
+ * @memberof DownloaderHelper
901
+ */
902
+ __getFileNameFromHeaders(headers, response) {
903
+ let fileName = '';
904
+
905
+ const fileNameAndEncodingRegExp = /.*filename\*=.*?'.*?'([^"].+?[^"])(?:(?:;)|$)/i; // match everything after the specified encoding behind a case-insensitive `filename*=`
906
+ const fileNameWithQuotesRegExp = /.*filename="(.*?)";?/i; // match everything inside the quotes behind a case-insensitive `filename=`
907
+ const fileNameWithoutQuotesRegExp = /.*filename=([^"].+?[^"])(?:(?:;)|$)/i; // match everything immediately after `filename=` that isn't surrounded by quotes and is followed by either a `;` or the end of the string
908
+
909
+ const ContentDispositionHeaderExists = headers.hasOwnProperty('content-disposition');
910
+ const fileNameAndEncodingMatch = !ContentDispositionHeaderExists ? null : headers['content-disposition'].match(fileNameAndEncodingRegExp);
911
+ const fileNameWithQuotesMatch = (!ContentDispositionHeaderExists || fileNameAndEncodingMatch) ? null : headers['content-disposition'].match(fileNameWithQuotesRegExp);
912
+ const fileNameWithoutQuotesMatch = (!ContentDispositionHeaderExists || fileNameAndEncodingMatch || fileNameWithQuotesMatch) ? null : headers['content-disposition'].match(fileNameWithoutQuotesRegExp);
913
+
914
+ // Get Filename
915
+ if (ContentDispositionHeaderExists && (fileNameAndEncodingMatch || fileNameWithQuotesMatch || fileNameWithoutQuotesMatch)) {
916
+
917
+ fileName = headers['content-disposition'];
918
+ fileName = fileName.trim();
919
+
920
+ if (fileNameAndEncodingMatch) {
921
+ fileName = fileNameAndEncodingMatch[1];
922
+ } else if (fileNameWithQuotesMatch) {
923
+ fileName = fileNameWithQuotesMatch[1];
924
+ } else if (fileNameWithoutQuotesMatch) {
925
+ fileName = fileNameWithoutQuotesMatch[1];
926
+ }
927
+
928
+ fileName = fileName.replace(/[/\\]/g, '');
929
+
930
+ } else {
931
+
932
+ if (path__namespace.basename(new url.URL(this.requestURL).pathname).length > 0) {
933
+ fileName = path__namespace.basename(new url.URL(this.requestURL).pathname);
934
+ } else {
935
+ fileName = `${new url.URL(this.requestURL).hostname}.html`;
936
+ }
937
+ }
938
+
939
+ return (
940
+ (this.__opts.fileName)
941
+ ? this.__getFileNameFromOpts(fileName, response)
942
+ : fileName.replace(/\.*$/, '') // remove any potential trailing '.' (just to be sure)
943
+ )
944
+ }
945
+
946
+ /**
947
+ *
948
+ *
949
+ * @param {String} fileName
950
+ * @returns {String}
951
+ * @memberof DownloaderHelper
952
+ */
953
+ __getFilePath(fileName) {
954
+ const currentPath = path__namespace.join(this.__destFolder, fileName);
955
+ let filePath = currentPath;
956
+
957
+ if (!this.__opts.override && this.state !== this.__states.RESUMED) {
958
+ filePath = this.__uniqFileNameSync(filePath);
959
+
960
+ if (currentPath !== filePath) {
961
+ this.emit('renamed', {
962
+ 'path': filePath,
963
+ 'fileName': filePath.split(path__namespace.sep).pop(),
964
+ 'prevPath': currentPath,
965
+ 'prevFileName': currentPath.split(path__namespace.sep).pop()
966
+ });
967
+ }
968
+ }
969
+
970
+ return filePath;
971
+ }
972
+
973
+
974
+ /**
975
+ *
976
+ *
977
+ * @param {String} fileName
978
+ * @returns {String}
979
+ * @memberof DownloaderHelper
980
+ */
981
+ __getFileNameFromOpts(fileName, response) {
982
+
983
+ if (!this.__opts.fileName) {
984
+ return fileName;
985
+ } else if (typeof this.__opts.fileName === 'string') {
986
+ return this.__opts.fileName;
987
+ } else if (typeof this.__opts.fileName === 'function') {
988
+ const currentPath = path__namespace.join(this.__destFolder, fileName);
989
+ if ((response && response.headers) || (this.__response && this.__response.headers)) {
990
+ return this.__opts.fileName(fileName, currentPath, (response ? response : this.__response).headers['content-type']);
991
+ } else {
992
+ return this.__opts.fileName(fileName, currentPath);
993
+ }
994
+ } else if (typeof this.__opts.fileName === 'object') {
995
+
996
+ const fileNameOpts = this.__opts.fileName; // { name:string, ext:true|false|string}
997
+ const name = fileNameOpts.name;
998
+ const ext = fileNameOpts.hasOwnProperty('ext')
999
+ ? fileNameOpts.ext : false;
1000
+
1001
+ if (typeof ext === 'string') {
1002
+ return `${name}.${ext}`;
1003
+ } else if (typeof ext === 'boolean') {
1004
+ // true: use the 'name' as full file name
1005
+ // false (default) only replace the name
1006
+ if (ext) {
1007
+ return name;
1008
+ } else {
1009
+ const _ext = fileName.includes('.') ? fileName.split('.').pop() : ''; // make sure there is a '.' in the fileName string
1010
+ return _ext !== '' ? `${name}.${_ext}` : name; // if there is no extension, replace the whole file name
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ return fileName;
1016
+ }
1017
+
1018
+ /**
1019
+ *
1020
+ *
1021
+ * @param {Number} receivedBytes
1022
+ * @memberof DownloaderHelper
1023
+ */
1024
+ __calculateStats(receivedBytes) {
1025
+ const currentTime = new Date();
1026
+ const elaspsedTime = currentTime - this.__statsEstimate.time;
1027
+ const throttleElapseTime = currentTime - this.__statsEstimate.throttleTime;
1028
+ const total = this.__total || 0;
1029
+
1030
+ if (!receivedBytes) {
1031
+ return;
1032
+ }
1033
+
1034
+ this.__downloaded += receivedBytes;
1035
+ this.__progress = total === 0 ? 0 : (this.__downloaded / total) * 100;
1036
+
1037
+ // Calculate the speed every second or if finished
1038
+ if (this.__downloaded === total || elaspsedTime > 1000) {
1039
+ this.__statsEstimate.time = currentTime;
1040
+ this.__statsEstimate.bytes = this.__downloaded - this.__statsEstimate.prevBytes;
1041
+ this.__statsEstimate.prevBytes = this.__downloaded;
1042
+ }
1043
+
1044
+ if (this.__downloaded === total || throttleElapseTime > this.__opts.progressThrottle) {
1045
+ this.__statsEstimate.throttleTime = currentTime;
1046
+ this.emit('progress.throttled', this.getStats());
1047
+ }
1048
+
1049
+ // emit the progress
1050
+ this.emit('progress', this.getStats());
1051
+ }
1052
+
1053
+ /**
1054
+ *
1055
+ *
1056
+ * @param {String} state
1057
+ * @memberof DownloaderHelper
1058
+ */
1059
+ __setState(state) {
1060
+ this.state = state;
1061
+ this.emit('stateChanged', this.state);
1062
+ }
1063
+
1064
+ /**
1065
+ *
1066
+ *
1067
+ * @param {String} method
1068
+ * @param {String} url
1069
+ * @param {Object} [headers={}]
1070
+ * @returns {Object}
1071
+ * @memberof DownloaderHelper
1072
+ */
1073
+ __getReqOptions(method, url$1, headers = {}) {
1074
+ const urlParse = new url.URL(url$1);
1075
+ const options = {
1076
+ protocol: urlParse.protocol,
1077
+ host: urlParse.hostname,
1078
+ port: urlParse.port,
1079
+ path: urlParse.pathname + urlParse.search,
1080
+ method,
1081
+ };
1082
+
1083
+ if (headers) {
1084
+ options['headers'] = headers;
1085
+ }
1086
+
1087
+ return options;
1088
+ }
1089
+
1090
+ /**
1091
+ *
1092
+ *
1093
+ * @param {String} filePath
1094
+ * @returns {Number}
1095
+ * @memberof DownloaderHelper
1096
+ */
1097
+ __getFilesizeInBytes(filePath) {
1098
+ try {
1099
+ // 'throwIfNoEntry' was implemented on Node.js v14.17.0
1100
+ // so we added try/catch in case is using an older version
1101
+ const stats = fs__namespace.statSync(filePath, { throwIfNoEntry: false });
1102
+ const fileSizeInBytes = stats.size || 0;
1103
+ return fileSizeInBytes;
1104
+ } catch (err) {
1105
+ // mostly probably the file doesn't exist
1106
+ this.emit('warning', err);
1107
+ }
1108
+ return 0;
1109
+ }
1110
+
1111
+ /**
1112
+ *
1113
+ *
1114
+ * @param {String} url
1115
+ * @param {String} destFolder
1116
+ * @returns {Boolean|Error}
1117
+ * @memberof DownloaderHelper
1118
+ */
1119
+ __validate(url, destFolder) {
1120
+
1121
+ if (typeof url !== 'string') {
1122
+ throw new Error('URL should be an string');
1123
+ }
1124
+
1125
+ if (url.trim() === '') {
1126
+ throw new Error("URL couldn't be empty");
1127
+ }
1128
+
1129
+ if (typeof destFolder !== 'string') {
1130
+ throw new Error('Destination Folder should be an string');
1131
+ }
1132
+
1133
+ if (destFolder.trim() === '') {
1134
+ throw new Error("Destination Folder couldn't be empty");
1135
+ }
1136
+
1137
+ if (!fs__namespace.existsSync(destFolder)) {
1138
+ throw new Error('Destination Folder must exist');
1139
+ }
1140
+ const isMas = process.mas === true;
1141
+ if (isMas) {
1142
+ return true;
1143
+ }
1144
+ const stats = fs__namespace.statSync(destFolder);
1145
+ if (!stats.isDirectory()) {
1146
+ throw new Error('Destination Folder must be a directory');
1147
+ }
1148
+
1149
+ try {
1150
+ fs__namespace.accessSync(destFolder, fs__namespace.constants.W_OK);
1151
+ } catch (e) {
1152
+ throw new Error('Destination Folder must be writable');
1153
+ }
1154
+
1155
+ return true;
1156
+ }
1157
+
1158
+ /**
1159
+ *
1160
+ *
1161
+ * @param {String} url
1162
+ * @memberof DownloaderHelper
1163
+ */
1164
+ __initProtocol(url) {
1165
+ const defaultOpts = this.__getReqOptions(this.__opts.method, url, this.__headers);
1166
+ this.requestURL = url;
1167
+
1168
+ if (url.indexOf('https://') > -1) {
1169
+ this.__protocol = https__namespace;
1170
+ defaultOpts.agent = new https__namespace.Agent({ keepAlive: false });
1171
+ this.__reqOptions = Object.assign({}, defaultOpts, this.__opts.httpsRequestOptions);
1172
+ } else {
1173
+ this.__protocol = http__namespace;
1174
+ defaultOpts.agent = new http__namespace.Agent({ keepAlive: false });
1175
+ this.__reqOptions = Object.assign({}, defaultOpts, this.__opts.httpRequestOptions);
1176
+ }
1177
+
1178
+ }
1179
+
1180
+ /**
1181
+ *
1182
+ *
1183
+ * @param {String} path
1184
+ * @returns {String}
1185
+ * @memberof DownloaderHelper
1186
+ */
1187
+ __uniqFileNameSync(path) {
1188
+ if (typeof path !== 'string' || path === '') {
1189
+ return path;
1190
+ }
1191
+
1192
+ try {
1193
+ // if access fail, the file doesnt exist yet
1194
+ fs__namespace.accessSync(path, fs__namespace.F_OK);
1195
+ const pathInfo = path.match(/(.*)(\([0-9]+\))(\..*)$/);
1196
+ let base = pathInfo ? pathInfo[1].trim() : path;
1197
+ let suffix = pathInfo ? parseInt(pathInfo[2].replace(/\(|\)/, '')) : 0;
1198
+ let ext = path.split('.').pop();
1199
+
1200
+ if (ext !== path && ext.length > 0) {
1201
+ ext = '.' + ext;
1202
+ base = base.replace(ext, '');
1203
+ } else {
1204
+ ext = '';
1205
+ }
1206
+
1207
+ // generate a new path until it doesn't exist
1208
+ return this.__uniqFileNameSync(base + ' (' + (++suffix) + ')' + ext);
1209
+ } catch (err) {
1210
+ return path;
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ *
1216
+ *
1217
+ * @returns {Promise<void>}
1218
+ * @memberof DownloaderHelper
1219
+ */
1220
+ __removeFile() {
1221
+ return new Promise(resolve => {
1222
+ if (!this.__fileStream) {
1223
+ return resolve();
1224
+ }
1225
+ this.__fileStream.close((err) => {
1226
+ if (err) {
1227
+ this.emit('warning', err);
1228
+ }
1229
+ if (this.__opts.removeOnFail) {
1230
+ return fs__namespace.access(this.__filePath, _accessErr => {
1231
+ // if can't access, probably is not created yet
1232
+ if (_accessErr) {
1233
+ return resolve();
1234
+ }
1235
+
1236
+ fs__namespace.unlink(this.__filePath, (_err) => {
1237
+ if (_err) {
1238
+ this.emit('warning', err);
1239
+ }
1240
+ resolve();
1241
+ });
1242
+ });
1243
+ }
1244
+ resolve();
1245
+ });
1246
+ });
1247
+ }
1248
+
1249
+ /**
1250
+ *
1251
+ *
1252
+ * @memberof DownloaderHelper
1253
+ */
1254
+ __requestAbort() {
1255
+ this.__isAborted = true;
1256
+ if (this.__retryTimeout) {
1257
+ clearTimeout(this.__retryTimeout);
1258
+ this.__retryTimeout = null;
1259
+ }
1260
+
1261
+ if (this.__response) {
1262
+ this.__response.destroy();
1263
+ }
1264
+
1265
+ if (this.__request) {
1266
+ // from node => v13.14.X
1267
+ if (this.__request.destroy) {
1268
+ this.__request.destroy();
1269
+ } else {
1270
+ this.__request.abort();
1271
+ }
1272
+ }
1273
+ }
1274
+
1275
+ /**
1276
+ * @memberof DownloaderHelper
1277
+ */
1278
+ __emitStop() {
1279
+ this.__resolvePending();
1280
+ this.__setState(this.__states.STOPPED);
1281
+ this.emit('stop');
1282
+ }
1283
+ }
1284
+
1285
+ exports.DH_STATES = DH_STATES;
1286
+ exports.DownloaderHelper = DownloaderHelper;
1287
+ //# sourceMappingURL=index.js.map