grab-url 0.9.139 โ 0.9.141
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/package.json +1 -1
- package/src/grab-url-cli.js +61 -43
package/package.json
CHANGED
package/src/grab-url-cli.js
CHANGED
|
@@ -141,7 +141,7 @@ ensureStateDirectoryExists() {
|
|
|
141
141
|
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
142
142
|
}
|
|
143
143
|
} catch (error) {
|
|
144
|
-
console.log(this.colors.warning('
|
|
144
|
+
console.log(this.colors.warning('Could not create state directory, using current directory'));
|
|
145
145
|
this.stateDir = process.cwd();
|
|
146
146
|
}
|
|
147
147
|
}
|
|
@@ -166,7 +166,7 @@ cleanupStateFile(stateFilePath) {
|
|
|
166
166
|
fs.unlinkSync(stateFilePath);
|
|
167
167
|
}
|
|
168
168
|
} catch (error) {
|
|
169
|
-
console.log(this.colors.warning('
|
|
169
|
+
console.log(this.colors.warning('Could not clean up state file'));
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -291,11 +291,17 @@ calculateBarSize(spinnerFrame, baseBarSize = 20) {
|
|
|
291
291
|
*/
|
|
292
292
|
async checkServerSupport(url) {
|
|
293
293
|
try {
|
|
294
|
+
// Create a timeout controller
|
|
295
|
+
const timeoutController = new AbortController();
|
|
296
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), 10000); // 10 second timeout
|
|
297
|
+
|
|
294
298
|
const response = await fetch(url, {
|
|
295
299
|
method: 'HEAD',
|
|
296
|
-
signal:
|
|
300
|
+
signal: timeoutController.signal
|
|
297
301
|
});
|
|
298
302
|
|
|
303
|
+
clearTimeout(timeoutId);
|
|
304
|
+
|
|
299
305
|
if (!response.ok) {
|
|
300
306
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
301
307
|
}
|
|
@@ -313,7 +319,11 @@ async checkServerSupport(url) {
|
|
|
313
319
|
headers: response.headers
|
|
314
320
|
};
|
|
315
321
|
} catch (error) {
|
|
316
|
-
|
|
322
|
+
if (error.name === 'AbortError') {
|
|
323
|
+
console.log(this.colors.warning('Server check timed out, proceeding with regular download'));
|
|
324
|
+
} else {
|
|
325
|
+
console.log(this.colors.warning('Could not check server resume support, proceeding with regular download'));
|
|
326
|
+
}
|
|
317
327
|
return {
|
|
318
328
|
supportsResume: false,
|
|
319
329
|
totalSize: 0,
|
|
@@ -336,7 +346,7 @@ loadDownloadState(stateFilePath) {
|
|
|
336
346
|
return JSON.parse(stateData);
|
|
337
347
|
}
|
|
338
348
|
} catch (error) {
|
|
339
|
-
console.log(this.colors.warning('
|
|
349
|
+
console.log(this.colors.warning('Could not load download state, starting fresh'));
|
|
340
350
|
}
|
|
341
351
|
return null;
|
|
342
352
|
}
|
|
@@ -350,7 +360,7 @@ saveDownloadState(stateFilePath, state) {
|
|
|
350
360
|
try {
|
|
351
361
|
fs.writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
|
|
352
362
|
} catch (error) {
|
|
353
|
-
console.log(this.colors.warning('
|
|
363
|
+
console.log(this.colors.warning('Could not save download state'));
|
|
354
364
|
}
|
|
355
365
|
}
|
|
356
366
|
|
|
@@ -366,7 +376,7 @@ getPartialFileSize(filePath) {
|
|
|
366
376
|
return stats.size;
|
|
367
377
|
}
|
|
368
378
|
} catch (error) {
|
|
369
|
-
console.log(this.colors.warning('
|
|
379
|
+
console.log(this.colors.warning('Could not read partial file size'));
|
|
370
380
|
}
|
|
371
381
|
return 0;
|
|
372
382
|
}
|
|
@@ -960,12 +970,20 @@ async downloadSingleFileWithBar(fileBar, masterBar, totalFiles, totalTracking) {
|
|
|
960
970
|
headers['Range'] = `bytes=${startByte}-`;
|
|
961
971
|
}
|
|
962
972
|
|
|
963
|
-
// Make the fetch request
|
|
973
|
+
// Make the fetch request with timeout
|
|
974
|
+
const timeoutController = new AbortController();
|
|
975
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), 30000); // 30 second timeout
|
|
976
|
+
|
|
977
|
+
// Combine abort and timeout signals
|
|
978
|
+
const combinedSignal = abortController.signal;
|
|
979
|
+
|
|
964
980
|
const response = await fetch(url, {
|
|
965
981
|
headers,
|
|
966
|
-
signal:
|
|
982
|
+
signal: combinedSignal
|
|
967
983
|
});
|
|
968
984
|
|
|
985
|
+
clearTimeout(timeoutId);
|
|
986
|
+
|
|
969
987
|
if (!response.ok) {
|
|
970
988
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
971
989
|
}
|
|
@@ -1135,7 +1153,7 @@ async downloadSingleFileWithBar(fileBar, masterBar, totalFiles, totalTracking) {
|
|
|
1135
1153
|
});
|
|
1136
1154
|
|
|
1137
1155
|
// Don't clean up partial file on error - allow resume
|
|
1138
|
-
console.log(this.colors.info(
|
|
1156
|
+
console.log(this.colors.info(`Partial download saved for ${filename}. Restart to resume.`));
|
|
1139
1157
|
throw error;
|
|
1140
1158
|
}
|
|
1141
1159
|
}
|
|
@@ -1156,7 +1174,7 @@ async downloadFile(url, outputPath) {
|
|
|
1156
1174
|
// Start with a random ora spinner animation
|
|
1157
1175
|
const randomOraSpinner = this.getRandomOraSpinner();
|
|
1158
1176
|
this.loadingSpinner = ora({
|
|
1159
|
-
text: this.colors.primary('
|
|
1177
|
+
text: this.colors.primary('Checking server capabilities...'),
|
|
1160
1178
|
spinner: randomOraSpinner,
|
|
1161
1179
|
color: 'cyan'
|
|
1162
1180
|
}).start();
|
|
@@ -1183,10 +1201,10 @@ async downloadFile(url, outputPath) {
|
|
|
1183
1201
|
if (fileUnchanged && partialSize < serverInfo.totalSize) {
|
|
1184
1202
|
startByte = partialSize;
|
|
1185
1203
|
resuming = true;
|
|
1186
|
-
this.loadingSpinner.succeed(this.colors.success(
|
|
1187
|
-
console.log(this.colors.info(
|
|
1204
|
+
this.loadingSpinner.succeed(this.colors.success(`Found partial download: ${this.formatBytes(partialSize)} of ${this.formatTotal(serverInfo.totalSize)}`));
|
|
1205
|
+
console.log(this.colors.info(`Resuming download from ${this.formatBytes(startByte)}`));
|
|
1188
1206
|
} else {
|
|
1189
|
-
this.loadingSpinner.warn(this.colors.warning('
|
|
1207
|
+
this.loadingSpinner.warn(this.colors.warning('File changed on server, starting fresh download'));
|
|
1190
1208
|
// Clean up partial file and state
|
|
1191
1209
|
if (fs.existsSync(tempFilePath)) {
|
|
1192
1210
|
fs.unlinkSync(tempFilePath);
|
|
@@ -1196,7 +1214,7 @@ async downloadFile(url, outputPath) {
|
|
|
1196
1214
|
} else {
|
|
1197
1215
|
this.loadingSpinner.stop();
|
|
1198
1216
|
if (partialSize > 0) {
|
|
1199
|
-
console.log(this.colors.warning('
|
|
1217
|
+
console.log(this.colors.warning('Server does not support resumable downloads, starting fresh'));
|
|
1200
1218
|
// Clean up partial file
|
|
1201
1219
|
if (fs.existsSync(tempFilePath)) {
|
|
1202
1220
|
fs.unlinkSync(tempFilePath);
|
|
@@ -1210,12 +1228,17 @@ async downloadFile(url, outputPath) {
|
|
|
1210
1228
|
headers['Range'] = `bytes=${startByte}-`;
|
|
1211
1229
|
}
|
|
1212
1230
|
|
|
1213
|
-
// Make the fetch request
|
|
1231
|
+
// Make the fetch request with timeout
|
|
1232
|
+
const timeoutController = new AbortController();
|
|
1233
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), 30000); // 30 second timeout
|
|
1234
|
+
|
|
1214
1235
|
const response = await fetch(url, {
|
|
1215
1236
|
headers,
|
|
1216
1237
|
signal: this.abortController.signal
|
|
1217
1238
|
});
|
|
1218
1239
|
|
|
1240
|
+
clearTimeout(timeoutId);
|
|
1241
|
+
|
|
1219
1242
|
if (!response.ok) {
|
|
1220
1243
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1221
1244
|
}
|
|
@@ -1227,9 +1250,9 @@ async downloadFile(url, outputPath) {
|
|
|
1227
1250
|
|
|
1228
1251
|
if (!resuming) {
|
|
1229
1252
|
if (totalSize === 0) {
|
|
1230
|
-
console.log(this.colors.warning('
|
|
1253
|
+
console.log(this.colors.warning('Warning: Content-Length not provided, progress will be estimated'));
|
|
1231
1254
|
} else {
|
|
1232
|
-
console.log(this.colors.info(
|
|
1255
|
+
console.log(this.colors.info(`File size: ${this.formatTotal(totalSize)}`));
|
|
1233
1256
|
}
|
|
1234
1257
|
}
|
|
1235
1258
|
|
|
@@ -1416,36 +1439,31 @@ async downloadFile(url, outputPath) {
|
|
|
1416
1439
|
// Clean up state file
|
|
1417
1440
|
this.cleanupStateFile(stateFilePath);
|
|
1418
1441
|
|
|
1419
|
-
// Success
|
|
1420
|
-
console.log(this.colors.success('
|
|
1421
|
-
console.log(this.colors.primary('
|
|
1422
|
-
console.log(this.colors.purple('
|
|
1442
|
+
// Success message
|
|
1443
|
+
console.log(this.colors.success('Download completed!'));
|
|
1444
|
+
console.log(this.colors.primary('File saved to: ') + chalk.underline(outputPath));
|
|
1445
|
+
console.log(this.colors.purple('Total size: ') + this.formatBytes(downloaded));
|
|
1423
1446
|
|
|
1424
1447
|
if (resuming) {
|
|
1425
|
-
console.log(this.colors.info('
|
|
1426
|
-
console.log(this.colors.info('
|
|
1448
|
+
console.log(this.colors.info('Resumed from: ') + this.formatBytes(startByte));
|
|
1449
|
+
console.log(this.colors.info('Downloaded this session: ') + this.formatBytes(sessionDownloaded));
|
|
1427
1450
|
}
|
|
1428
|
-
|
|
1429
|
-
// Random success emoji
|
|
1430
|
-
const celebrationEmojis = ['๐ฅณ', '๐', '๐', '๐', '๐ฏ', '๐', 'โจ', '๐ฅ'];
|
|
1431
|
-
const randomEmoji = celebrationEmojis[Math.floor(Math.random() * celebrationEmojis.length)];
|
|
1432
|
-
console.log(this.colors.success(`${randomEmoji} Successfully downloaded! ${randomEmoji}`));
|
|
1433
1451
|
|
|
1434
1452
|
} catch (error) {
|
|
1435
1453
|
if (this.loadingSpinner && this.loadingSpinner.isSpinning) {
|
|
1436
|
-
this.loadingSpinner.fail(this.colors.error('
|
|
1454
|
+
this.loadingSpinner.fail(this.colors.error('Connection failed'));
|
|
1437
1455
|
}
|
|
1438
1456
|
if (this.progressBar) {
|
|
1439
1457
|
this.progressBar.stop();
|
|
1440
1458
|
}
|
|
1441
1459
|
|
|
1442
1460
|
// Don't clean up partial file on error - allow resume
|
|
1443
|
-
console.error(this.colors.error.bold('
|
|
1461
|
+
console.error(this.colors.error.bold('Download failed: ') + this.colors.warning(error.message));
|
|
1444
1462
|
|
|
1445
1463
|
if (error.name === 'AbortError') {
|
|
1446
|
-
console.log(this.colors.info('
|
|
1464
|
+
console.log(this.colors.info('Download state saved. You can resume later by running the same command.'));
|
|
1447
1465
|
} else {
|
|
1448
|
-
console.log(this.colors.info('
|
|
1466
|
+
console.log(this.colors.info('Partial download saved. Restart to resume from where it left off.'));
|
|
1449
1467
|
}
|
|
1450
1468
|
|
|
1451
1469
|
throw error;
|
|
@@ -1891,7 +1909,7 @@ for (let i = 0; i < urlIndices.length; i++) {
|
|
|
1891
1909
|
downloads.push({ url, outputPath });
|
|
1892
1910
|
}
|
|
1893
1911
|
|
|
1894
|
-
console.log(chalk.cyan.bold(
|
|
1912
|
+
console.log(chalk.cyan.bold(`Detected ${downloads.length} download(s):`));
|
|
1895
1913
|
downloads.forEach((download, index) => {
|
|
1896
1914
|
console.log(` ${index + 1}. ${chalk.yellow(download.url)} (URL)`);
|
|
1897
1915
|
});
|
|
@@ -1899,7 +1917,7 @@ console.log('');
|
|
|
1899
1917
|
|
|
1900
1918
|
// Handle multiple downloads
|
|
1901
1919
|
if (downloads.length > 1) {
|
|
1902
|
-
console.log(chalk.blue.bold(
|
|
1920
|
+
console.log(chalk.blue.bold(`Multiple downloads detected: ${downloads.length} files\n`));
|
|
1903
1921
|
|
|
1904
1922
|
// Prepare download objects
|
|
1905
1923
|
const downloadObjects = downloads.map((download, index) => {
|
|
@@ -1932,7 +1950,7 @@ if (downloads.length > 1) {
|
|
|
1932
1950
|
});
|
|
1933
1951
|
|
|
1934
1952
|
// Show download queue
|
|
1935
|
-
|
|
1953
|
+
console.log(chalk.cyan.bold('\nDownload Queue:'));
|
|
1936
1954
|
downloadObjects.forEach((downloadObj, index) => {
|
|
1937
1955
|
console.log(` ${chalk.yellow((index + 1).toString().padStart(2))}. ${chalk.green(downloadObj.filename)} ${chalk.gray('โ')} ${downloadObj.outputPath}`);
|
|
1938
1956
|
});
|
|
@@ -1942,13 +1960,13 @@ if (downloads.length > 1) {
|
|
|
1942
1960
|
await downloader.downloadMultipleFiles(downloadObjects);
|
|
1943
1961
|
|
|
1944
1962
|
// Display individual file stats
|
|
1945
|
-
console.log(chalk.cyan.bold('\
|
|
1963
|
+
console.log(chalk.cyan.bold('\nFile Details:'));
|
|
1946
1964
|
downloadObjects.forEach((downloadObj) => {
|
|
1947
1965
|
displayFileStats(downloadObj.outputPath, downloader);
|
|
1948
1966
|
});
|
|
1949
1967
|
|
|
1950
1968
|
} catch (error) {
|
|
1951
|
-
console.error(chalk.red.bold('
|
|
1969
|
+
console.error(chalk.red.bold('Failed to download files: ') + chalk.yellow(error.message));
|
|
1952
1970
|
process.exit(1);
|
|
1953
1971
|
}
|
|
1954
1972
|
|
|
@@ -1975,26 +1993,26 @@ if (downloads.length > 1) {
|
|
|
1975
1993
|
|
|
1976
1994
|
// Check if file already exists
|
|
1977
1995
|
if (fs.existsSync(outputPath)) {
|
|
1978
|
-
console.log(chalk.yellow('
|
|
1996
|
+
console.log(chalk.yellow('File already exists: ') + outputPath);
|
|
1979
1997
|
console.log(chalk.gray(' Continuing will overwrite the existing file...'));
|
|
1980
1998
|
}
|
|
1981
1999
|
|
|
1982
|
-
console.log(chalk.green('\
|
|
2000
|
+
console.log(chalk.green('\nTarget: ') + chalk.bold(outputPath));
|
|
1983
2001
|
|
|
1984
2002
|
try {
|
|
1985
2003
|
await downloader.downloadFile(url, outputPath);
|
|
1986
2004
|
displayFileStats(outputPath, downloader);
|
|
1987
2005
|
|
|
1988
2006
|
} catch (error) {
|
|
1989
|
-
console.error(chalk.red.bold('
|
|
2007
|
+
console.error(chalk.red.bold('Failed to download file: ') + chalk.yellow(error.message));
|
|
1990
2008
|
|
|
1991
2009
|
// Clean up partial file if it exists
|
|
1992
2010
|
if (fs.existsSync(outputPath)) {
|
|
1993
2011
|
try {
|
|
1994
2012
|
fs.unlinkSync(outputPath);
|
|
1995
|
-
console.log(chalk.gray('
|
|
2013
|
+
console.log(chalk.gray('Cleaned up partial download'));
|
|
1996
2014
|
} catch (cleanupError) {
|
|
1997
|
-
|
|
2015
|
+
console.log(chalk.yellow('Could not clean up partial file: ') + cleanupError.message);
|
|
1998
2016
|
}
|
|
1999
2017
|
}
|
|
2000
2018
|
|