git-ripper 1.4.4 → 1.4.6
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/README.md +87 -4
- package/package.json +2 -4
- package/src/archiver.js +24 -2
- package/src/downloader.js +136 -81
- package/src/index.js +35 -13
package/README.md
CHANGED
|
@@ -32,10 +32,14 @@ Have you ever needed just a single component from a massive repository? Or wante
|
|
|
32
32
|
## Features
|
|
33
33
|
|
|
34
34
|
- **Selective Downloads**: Fetch specific folders instead of entire repositories
|
|
35
|
+
- **Resume Interrupted Downloads**: Automatically resume downloads that were interrupted or failed
|
|
36
|
+
- **Progress Tracking**: Visual progress indicators with file-by-file download status
|
|
37
|
+
- **File Integrity Verification**: Ensures downloaded files are complete and uncorrupted
|
|
35
38
|
- **Directory Structure**: Preserves complete folder structure
|
|
36
39
|
- **Custom Output**: Specify your preferred output directory
|
|
37
40
|
- **Branch Support**: Works with any branch, not just the default one
|
|
38
41
|
- **Archive Export**: Create ZIP archives of downloaded content
|
|
42
|
+
- **Checkpoint Management**: View and manage saved download progress
|
|
39
43
|
- **Simple Interface**: Clean, intuitive command-line experience
|
|
40
44
|
- **Lightweight**: Minimal dependencies and fast execution
|
|
41
45
|
- **No Authentication**: Works with public repositories without requiring credentials
|
|
@@ -94,6 +98,9 @@ git-ripper https://github.com/username/repository/tree/branch/folder --zip="my-a
|
|
|
94
98
|
| -------------------------- | ---------------------------------------- | ----------------- |
|
|
95
99
|
| `-o, --output <directory>` | Specify output directory | Current directory |
|
|
96
100
|
| `--zip [filename]` | Create ZIP archive of downloaded content | - |
|
|
101
|
+
| `--no-resume` | Disable resume functionality | - |
|
|
102
|
+
| `--force-restart` | Ignore existing checkpoints and restart | - |
|
|
103
|
+
| `--list-checkpoints` | List all saved download checkpoints | - |
|
|
97
104
|
| `-V, --version` | Show version number | - |
|
|
98
105
|
| `-h, --help` | Show help | - |
|
|
99
106
|
|
|
@@ -137,14 +144,67 @@ git-ripper https://github.com/facebook/react/tree/main/packages/react-dom --zip
|
|
|
137
144
|
git-ripper https://github.com/microsoft/vscode/tree/main/build --zip="vscode-build.zip"
|
|
138
145
|
```
|
|
139
146
|
|
|
147
|
+
## Resume Downloads
|
|
148
|
+
|
|
149
|
+
Git-ripper now supports resuming interrupted downloads, making it perfect for large folders or unstable network connections.
|
|
150
|
+
|
|
151
|
+
### Automatic Resume (Default Behavior)
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Start a download
|
|
155
|
+
git-ripper https://github.com/microsoft/vscode/tree/main/src/vs/workbench
|
|
156
|
+
|
|
157
|
+
# If interrupted (Ctrl+C, network issues, etc.), simply run the same command again
|
|
158
|
+
git-ripper https://github.com/microsoft/vscode/tree/main/src/vs/workbench
|
|
159
|
+
# It will automatically resume from where it left off
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Force Restart
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Ignore any existing progress and start fresh
|
|
166
|
+
git-ripper https://github.com/microsoft/vscode/tree/main/src/vs/workbench --force-restart
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Disable Resume
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Use traditional behavior without resume functionality
|
|
173
|
+
git-ripper https://github.com/microsoft/vscode/tree/main/src/vs/workbench --no-resume
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Manage Checkpoints
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# List all saved download progress
|
|
180
|
+
git-ripper --list-checkpoints
|
|
181
|
+
|
|
182
|
+
# Output shows:
|
|
183
|
+
# 1. ID: a1b2c3d4
|
|
184
|
+
# URL: https://github.com/microsoft/vscode/tree/main/src/vs/workbench
|
|
185
|
+
# Progress: 45/120 files
|
|
186
|
+
# Last Updated: 2025-06-04T10:30:00Z
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Resume Features
|
|
190
|
+
|
|
191
|
+
- **Automatic Progress Saving**: Downloads are checkpointed every few files
|
|
192
|
+
- **File Integrity Verification**: Ensures existing files are complete and valid
|
|
193
|
+
- **Smart Recovery**: Detects corrupted or incomplete files and re-downloads them
|
|
194
|
+
- **Multi-Download Support**: Manage multiple concurrent download projects
|
|
195
|
+
- **Progress Indicators**: Visual feedback showing completed vs remaining files
|
|
196
|
+
|
|
140
197
|
## How It Works
|
|
141
198
|
|
|
142
|
-
Git-ripper operates in
|
|
199
|
+
Git-ripper operates in five stages:
|
|
143
200
|
|
|
144
201
|
1. **URL Parsing**: Extracts repository owner, name, branch, and target folder path
|
|
145
|
-
2. **
|
|
146
|
-
3. **
|
|
147
|
-
4. **
|
|
202
|
+
2. **Resume Check**: Looks for existing download progress and validates already downloaded files
|
|
203
|
+
3. **API Request**: Uses GitHub's API to fetch the folder structure
|
|
204
|
+
4. **Content Download**: Retrieves each file individually while maintaining directory structure and saving progress
|
|
205
|
+
5. **Local Storage or Archiving**: Saves files to your specified output directory or creates an archive
|
|
206
|
+
|
|
207
|
+
The resume functionality uses checkpoint files stored in `.git_ripper_checkpoints/` to track download progress, file integrity hashes, and metadata for each download session.
|
|
148
208
|
|
|
149
209
|
## Configuration
|
|
150
210
|
|
|
@@ -178,6 +238,29 @@ Error: Path not found in repository
|
|
|
178
238
|
|
|
179
239
|
**Solution**: Verify the folder path exists in the specified branch and repository.
|
|
180
240
|
|
|
241
|
+
#### Resume Issues
|
|
242
|
+
|
|
243
|
+
If you encounter problems with resume functionality:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# Clear all checkpoints and start fresh
|
|
247
|
+
git-ripper https://github.com/owner/repo/tree/branch/folder --force-restart
|
|
248
|
+
|
|
249
|
+
# Or disable resume entirely
|
|
250
|
+
git-ripper https://github.com/owner/repo/tree/branch/folder --no-resume
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Corrupted Download
|
|
254
|
+
|
|
255
|
+
If files appear corrupted after resume:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# Force restart will re-download everything
|
|
259
|
+
git-ripper https://github.com/owner/repo/tree/branch/folder --force-restart
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The resume feature automatically detects and re-downloads corrupted files, but `--force-restart` ensures a completely clean download.
|
|
263
|
+
|
|
181
264
|
## Contributing
|
|
182
265
|
|
|
183
266
|
Contributions make the open-source community an amazing place to learn, inspire, and create. Any contributions to Git-ripper are **greatly appreciated**.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-ripper",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "CLI tool that lets you download specific folders from GitHub repositories without cloning the entire repo.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -29,15 +29,13 @@
|
|
|
29
29
|
"author": "sairajb",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"ansi-styles": "^6.2.1",
|
|
33
32
|
"archiver": "^6.0.1",
|
|
34
33
|
"axios": "^1.6.7",
|
|
35
34
|
"chalk": "^5.3.0",
|
|
36
35
|
"cli-progress": "^3.12.0",
|
|
37
36
|
"commander": "^12.0.0",
|
|
38
37
|
"p-limit": "^6.2.0",
|
|
39
|
-
"pretty-bytes": "^6.1.1"
|
|
40
|
-
"supports-color": "^9.4.0"
|
|
38
|
+
"pretty-bytes": "^6.1.1"
|
|
41
39
|
},
|
|
42
40
|
"repository": {
|
|
43
41
|
"type": "git",
|
package/src/archiver.js
CHANGED
|
@@ -144,10 +144,32 @@ export const downloadAndArchive = async (
|
|
|
144
144
|
// Create a temporary directory for the download
|
|
145
145
|
const tempDir = path.join(outputDir, `.temp-${Date.now()}`);
|
|
146
146
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
147
|
-
|
|
148
147
|
try {
|
|
149
148
|
// Download the folder contents
|
|
150
|
-
await downloadFolder(repoInfo, tempDir);
|
|
149
|
+
const downloadResult = await downloadFolder(repoInfo, tempDir);
|
|
150
|
+
|
|
151
|
+
// Check if download failed
|
|
152
|
+
if (downloadResult && !downloadResult.success) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Download failed: ${
|
|
155
|
+
downloadResult.failedFiles || 0
|
|
156
|
+
} files could not be downloaded`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if there's anything to archive
|
|
161
|
+
const files = fs
|
|
162
|
+
.readdirSync(tempDir, { recursive: true })
|
|
163
|
+
.filter((file) => {
|
|
164
|
+
const fullPath = path.join(tempDir, file);
|
|
165
|
+
return fs.statSync(fullPath).isFile();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (files.length === 0) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`No files to archive - download may have failed or repository is empty`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
151
173
|
|
|
152
174
|
// Determine archive filename
|
|
153
175
|
let archiveFileName = archiveName;
|
package/src/downloader.js
CHANGED
|
@@ -45,6 +45,7 @@ const getSpinnerFrame = () => {
|
|
|
45
45
|
* @param {string} branch - Branch name
|
|
46
46
|
* @param {string} folderPath - Path to the folder
|
|
47
47
|
* @returns {Promise<Array>} - Promise resolving to an array of file objects
|
|
48
|
+
* @throws {Error} - Throws error on API failures instead of returning empty array
|
|
48
49
|
*/
|
|
49
50
|
const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
50
51
|
let effectiveBranch = branch;
|
|
@@ -55,12 +56,9 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
55
56
|
const repoInfoResponse = await axios.get(repoInfoUrl);
|
|
56
57
|
effectiveBranch = repoInfoResponse.data.default_branch;
|
|
57
58
|
if (!effectiveBranch) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`Could not determine default branch for ${owner}/${repo}. Please specify a branch in the URL.`
|
|
61
|
-
)
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Could not determine default branch for ${owner}/${repo}. Please specify a branch in the URL.`
|
|
62
61
|
);
|
|
63
|
-
return [];
|
|
64
62
|
}
|
|
65
63
|
console.log(
|
|
66
64
|
chalk.blue(
|
|
@@ -68,12 +66,12 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
68
66
|
)
|
|
69
67
|
);
|
|
70
68
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
if (error.message.includes("Could not determine default branch")) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to fetch default branch for ${owner}/${repo}: ${error.message}`
|
|
75
74
|
);
|
|
76
|
-
return [];
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
@@ -109,54 +107,44 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
109
107
|
return response.data.tree.filter((item) => item.path.startsWith(prefix));
|
|
110
108
|
}
|
|
111
109
|
} catch (error) {
|
|
110
|
+
let errorMessage = "";
|
|
111
|
+
let isRateLimit = false;
|
|
112
|
+
|
|
112
113
|
if (error.response) {
|
|
113
114
|
// Handle specific HTTP error codes
|
|
114
115
|
switch (error.response.status) {
|
|
115
116
|
case 403:
|
|
116
117
|
if (error.response.headers["x-ratelimit-remaining"] === "0") {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
).toLocaleTimeString()} or add a GitHub token (feature coming soon).`
|
|
122
|
-
)
|
|
123
|
-
);
|
|
118
|
+
isRateLimit = true;
|
|
119
|
+
errorMessage = `GitHub API rate limit exceeded. Please wait until ${new Date(
|
|
120
|
+
parseInt(error.response.headers["x-ratelimit-reset"]) * 1000
|
|
121
|
+
).toLocaleTimeString()} or add a GitHub token (feature coming soon).`;
|
|
124
122
|
} else {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}`
|
|
130
|
-
)
|
|
131
|
-
);
|
|
123
|
+
errorMessage = `Access forbidden: ${
|
|
124
|
+
error.response.data.message ||
|
|
125
|
+
"Repository may be private or you may not have access"
|
|
126
|
+
}`;
|
|
132
127
|
}
|
|
133
128
|
break;
|
|
134
129
|
case 404:
|
|
135
|
-
|
|
136
|
-
chalk.red(
|
|
137
|
-
`Repository, branch, or folder not found: ${owner}/${repo}/${branch}/${folderPath}`
|
|
138
|
-
)
|
|
139
|
-
);
|
|
130
|
+
errorMessage = `Repository, branch, or folder not found: ${owner}/${repo}/${branch}/${folderPath}`;
|
|
140
131
|
break;
|
|
141
132
|
default:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
error.response.data.message || error.message
|
|
146
|
-
}`
|
|
147
|
-
)
|
|
148
|
-
);
|
|
133
|
+
errorMessage = `API error (${error.response.status}): ${
|
|
134
|
+
error.response.data.message || error.message
|
|
135
|
+
}`;
|
|
149
136
|
}
|
|
150
137
|
} else if (error.request) {
|
|
151
|
-
|
|
152
|
-
chalk.red(
|
|
153
|
-
`Network error: No response received from GitHub. Please check your internet connection.`
|
|
154
|
-
)
|
|
155
|
-
);
|
|
138
|
+
errorMessage = `Network error: No response received from GitHub. Please check your internet connection.`;
|
|
156
139
|
} else {
|
|
157
|
-
|
|
140
|
+
errorMessage = `Error preparing request: ${error.message}`;
|
|
158
141
|
}
|
|
159
|
-
|
|
142
|
+
|
|
143
|
+
// Always throw the error instead of returning empty array
|
|
144
|
+
const enrichedError = new Error(errorMessage);
|
|
145
|
+
enrichedError.isRateLimit = isRateLimit;
|
|
146
|
+
enrichedError.statusCode = error.response?.status;
|
|
147
|
+
throw enrichedError;
|
|
160
148
|
}
|
|
161
149
|
};
|
|
162
150
|
|
|
@@ -349,11 +337,15 @@ const downloadFolder = async (
|
|
|
349
337
|
const contents = await fetchFolderContents(owner, repo, branch, folderPath);
|
|
350
338
|
|
|
351
339
|
if (!contents || contents.length === 0) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
340
|
+
const message = `No files found in ${folderPath || "repository root"}`;
|
|
341
|
+
console.log(chalk.yellow(message));
|
|
342
|
+
// Don't print success message when no files are found - this might indicate an error
|
|
343
|
+
return {
|
|
344
|
+
success: true,
|
|
345
|
+
filesDownloaded: 0,
|
|
346
|
+
failedFiles: 0,
|
|
347
|
+
isEmpty: true,
|
|
348
|
+
};
|
|
357
349
|
}
|
|
358
350
|
|
|
359
351
|
// Filter for blob type (files)
|
|
@@ -361,15 +353,18 @@ const downloadFolder = async (
|
|
|
361
353
|
const totalFiles = files.length;
|
|
362
354
|
|
|
363
355
|
if (totalFiles === 0) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
356
|
+
const message = `No files found in ${
|
|
357
|
+
folderPath || "repository root"
|
|
358
|
+
} (only directories)`;
|
|
359
|
+
console.log(chalk.yellow(message));
|
|
360
|
+
// This is a legitimate case - directory exists but contains only subdirectories
|
|
361
|
+
console.log(chalk.green(`Directory structure downloaded successfully!`));
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
filesDownloaded: 0,
|
|
365
|
+
failedFiles: 0,
|
|
366
|
+
isEmpty: true,
|
|
367
|
+
};
|
|
373
368
|
}
|
|
374
369
|
|
|
375
370
|
console.log(
|
|
@@ -464,7 +459,6 @@ const downloadFolder = async (
|
|
|
464
459
|
// Count successful and failed downloads
|
|
465
460
|
const succeeded = results.filter((r) => r.success).length;
|
|
466
461
|
const failed = failedFiles.length;
|
|
467
|
-
|
|
468
462
|
if (failed > 0) {
|
|
469
463
|
console.log(
|
|
470
464
|
chalk.yellow(
|
|
@@ -485,15 +479,45 @@ const downloadFolder = async (
|
|
|
485
479
|
)
|
|
486
480
|
);
|
|
487
481
|
}
|
|
482
|
+
|
|
483
|
+
// Don't claim success if files failed to download
|
|
484
|
+
if (succeeded === 0) {
|
|
485
|
+
console.log(
|
|
486
|
+
chalk.red(`❌ Download failed: No files were downloaded successfully`)
|
|
487
|
+
);
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
filesDownloaded: succeeded,
|
|
491
|
+
failedFiles: failed,
|
|
492
|
+
isEmpty: false,
|
|
493
|
+
};
|
|
494
|
+
} else {
|
|
495
|
+
console.log(chalk.yellow(`⚠️ Download completed with errors`));
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
filesDownloaded: succeeded,
|
|
499
|
+
failedFiles: failed,
|
|
500
|
+
isEmpty: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
488
503
|
} else {
|
|
489
504
|
console.log(
|
|
490
|
-
chalk.green(
|
|
505
|
+
chalk.green(`✅ All ${succeeded} files downloaded successfully!`)
|
|
491
506
|
);
|
|
507
|
+
console.log(chalk.green(`Folder cloned successfully!`));
|
|
508
|
+
return {
|
|
509
|
+
success: true,
|
|
510
|
+
filesDownloaded: succeeded,
|
|
511
|
+
failedFiles: failed,
|
|
512
|
+
isEmpty: false,
|
|
513
|
+
};
|
|
492
514
|
}
|
|
493
|
-
|
|
494
|
-
console.log(chalk.green(`Folder cloned successfully!`));
|
|
495
515
|
} catch (error) {
|
|
496
|
-
|
|
516
|
+
// Log the specific error details
|
|
517
|
+
console.error(chalk.red(`❌ Error downloading folder: ${error.message}`));
|
|
518
|
+
|
|
519
|
+
// Re-throw the error so the main CLI can exit with proper error code
|
|
520
|
+
throw error;
|
|
497
521
|
}
|
|
498
522
|
};
|
|
499
523
|
|
|
@@ -576,13 +600,16 @@ const downloadFolderWithResume = async (
|
|
|
576
600
|
|
|
577
601
|
try {
|
|
578
602
|
const contents = await fetchFolderContents(owner, repo, branch, folderPath);
|
|
579
|
-
|
|
580
603
|
if (!contents || contents.length === 0) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
604
|
+
const message = `No files found in ${folderPath || "repository root"}`;
|
|
605
|
+
console.log(chalk.yellow(message));
|
|
606
|
+
// Don't print success message when no files are found - this might indicate an error
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
filesDownloaded: 0,
|
|
610
|
+
failedFiles: 0,
|
|
611
|
+
isEmpty: true,
|
|
612
|
+
};
|
|
586
613
|
}
|
|
587
614
|
|
|
588
615
|
// Filter for blob type (files)
|
|
@@ -590,15 +617,18 @@ const downloadFolderWithResume = async (
|
|
|
590
617
|
const totalFiles = files.length;
|
|
591
618
|
|
|
592
619
|
if (totalFiles === 0) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
620
|
+
const message = `No files found in ${
|
|
621
|
+
folderPath || "repository root"
|
|
622
|
+
} (only directories)`;
|
|
623
|
+
console.log(chalk.yellow(message));
|
|
624
|
+
// This is a legitimate case - directory exists but contains only subdirectories
|
|
625
|
+
console.log(chalk.green(`Directory structure downloaded successfully!`));
|
|
626
|
+
return {
|
|
627
|
+
success: true,
|
|
628
|
+
filesDownloaded: 0,
|
|
629
|
+
failedFiles: 0,
|
|
630
|
+
isEmpty: true,
|
|
631
|
+
};
|
|
602
632
|
}
|
|
603
633
|
|
|
604
634
|
// Create new checkpoint if none exists
|
|
@@ -748,7 +778,6 @@ const downloadFolderWithResume = async (
|
|
|
748
778
|
// Count results
|
|
749
779
|
const succeeded = checkpoint.downloadedFiles.length;
|
|
750
780
|
const failed = failedFiles.length;
|
|
751
|
-
|
|
752
781
|
if (failed > 0) {
|
|
753
782
|
console.log(
|
|
754
783
|
chalk.yellow(
|
|
@@ -766,21 +795,47 @@ const downloadFolderWithResume = async (
|
|
|
766
795
|
console.log(
|
|
767
796
|
chalk.blue(`💡 Run the same command again to retry failed downloads`)
|
|
768
797
|
);
|
|
798
|
+
|
|
799
|
+
// Don't claim success if files failed to download
|
|
800
|
+
if (succeeded === 0) {
|
|
801
|
+
console.log(
|
|
802
|
+
chalk.red(`❌ Download failed: No files were downloaded successfully`)
|
|
803
|
+
);
|
|
804
|
+
return {
|
|
805
|
+
success: false,
|
|
806
|
+
filesDownloaded: succeeded,
|
|
807
|
+
failedFiles: failed,
|
|
808
|
+
isEmpty: false,
|
|
809
|
+
};
|
|
810
|
+
} else {
|
|
811
|
+
console.log(chalk.yellow(`⚠️ Download completed with errors`));
|
|
812
|
+
return {
|
|
813
|
+
success: false,
|
|
814
|
+
filesDownloaded: succeeded,
|
|
815
|
+
failedFiles: failed,
|
|
816
|
+
isEmpty: false,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
769
819
|
} else {
|
|
770
820
|
console.log(
|
|
771
821
|
chalk.green(`🎉 All ${succeeded} files downloaded successfully!`)
|
|
772
822
|
);
|
|
773
823
|
resumeManager.cleanupCheckpoint(url, outputDir);
|
|
824
|
+
console.log(chalk.green(`Folder cloned successfully!`));
|
|
825
|
+
return {
|
|
826
|
+
success: true,
|
|
827
|
+
filesDownloaded: succeeded,
|
|
828
|
+
failedFiles: failed,
|
|
829
|
+
isEmpty: false,
|
|
830
|
+
};
|
|
774
831
|
}
|
|
775
|
-
|
|
776
|
-
console.log(chalk.green(`Folder cloned successfully!`));
|
|
777
832
|
} catch (error) {
|
|
778
833
|
// Save checkpoint on any error
|
|
779
834
|
if (checkpoint) {
|
|
780
835
|
resumeManager.saveCheckpoint(checkpoint);
|
|
781
836
|
}
|
|
782
837
|
|
|
783
|
-
console.error(chalk.red(
|
|
838
|
+
console.error(chalk.red(`❌ Error downloading folder: ${error.message}`));
|
|
784
839
|
throw error;
|
|
785
840
|
}
|
|
786
841
|
};
|
package/src/index.js
CHANGED
|
@@ -123,24 +123,46 @@ const initializeCLI = () => {
|
|
|
123
123
|
forceRestart: options.forceRestart || false,
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
} else {
|
|
130
|
-
console.log(`Downloading folder to: ${options.output}`);
|
|
126
|
+
let operationType = createArchive ? "archive" : "download";
|
|
127
|
+
let result = null;
|
|
128
|
+
let error = null;
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
downloadOptions
|
|
137
|
-
);
|
|
130
|
+
try {
|
|
131
|
+
if (createArchive) {
|
|
132
|
+
console.log(`Creating ZIP archive...`);
|
|
133
|
+
await downloadAndArchive(parsedUrl, options.output, archiveName);
|
|
138
134
|
} else {
|
|
139
|
-
|
|
135
|
+
console.log(`Downloading folder to: ${options.output}`);
|
|
136
|
+
if (downloadOptions.resume) {
|
|
137
|
+
result = await downloadFolderWithResume(
|
|
138
|
+
parsedUrl,
|
|
139
|
+
options.output,
|
|
140
|
+
downloadOptions
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
result = await downloadFolder(parsedUrl, options.output);
|
|
144
|
+
}
|
|
140
145
|
}
|
|
146
|
+
} catch (opError) {
|
|
147
|
+
error = opError;
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
|
|
150
|
+
// Consolidated result and error handling
|
|
151
|
+
if (error) {
|
|
152
|
+
const failMsg =
|
|
153
|
+
operationType === "archive"
|
|
154
|
+
? `❌ Archive creation failed: ${error.message}`
|
|
155
|
+
: `❌ Download failed: ${error.message}`;
|
|
156
|
+
console.error(chalk.red(failMsg));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
} else if (!createArchive && result && !result.success) {
|
|
159
|
+
console.error(chalk.red(`❌ Download failed`));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
} else if (!createArchive && result && result.isEmpty) {
|
|
162
|
+
console.log("Operation completed - no files to download!");
|
|
163
|
+
} else {
|
|
164
|
+
console.log("Operation completed successfully!");
|
|
165
|
+
}
|
|
144
166
|
} catch (error) {
|
|
145
167
|
console.error("Error:", error.message);
|
|
146
168
|
process.exit(1);
|