@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.
- package/esm/main/downloader.d.ts.map +1 -1
- package/esm/main/downloader.js +34 -23
- package/esm/main/downloader.js.map +1 -1
- package/esm/main/node-downloader-helper/index.js +1270 -0
- package/esm/main/node-downloader-helper/index.js.map +1 -0
- package/esm/main/node-downloader-helper.js +1255 -0
- package/esm/main/node-downloader-helper.js.map +1 -0
- package/main/downloader.d.ts.map +1 -1
- package/main/downloader.js +35 -24
- package/main/downloader.js.map +1 -1
- package/main/node-downloader-helper/index.js +1287 -0
- package/main/node-downloader-helper/index.js.map +1 -0
- package/main/node-downloader-helper.js +1272 -0
- package/main/node-downloader-helper.js.map +1 -0
- package/package.json +5 -5
|
@@ -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
|