Skip to content

Commit ba3a6de

Browse files
committed
errors handle refactor; muteLog option
1 parent a4cf076 commit ba3a6de

File tree

4 files changed

+96
-50
lines changed

4 files changed

+96
-50
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ You can also pass options as a third argument:
3434
* download-directory.github.io has no limit, but it can lead to IP blocking
3535
*/
3636
requests?: number;
37+
38+
/** Disable console logs */
39+
muteLog?: boolean;
3740
}
3841
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "github-directory-downloader",
3-
"version": "1.2.2",
3+
"version": "1.2.3",
44
"description": "Download just a sub directory from a GitHub repo",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/index.ts

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,14 @@ import fetch from 'node-fetch';
33
import { dirname, resolve } from 'path';
44
import { promisify } from 'util';
55

6-
const streamPipeline = promisify(require('stream').pipeline);
6+
import { Config, Stats, TreeItem } from './types';
77

8-
type TreeItem = {
9-
path: string;
10-
mode: string;
11-
type: string;
12-
sha: string;
13-
size: number;
14-
url: string;
15-
}
8+
const streamPipeline = promisify(require('stream').pipeline);
169

1710
// Matches '/<re/po>/tree/<ref>/<dir>'
1811
const urlParserRegex = /^[/]([^/]+)[/]([^/]+)[/]tree[/]([^/]+)[/](.*)/;
1912

20-
async function fetchRepoInfo(repo: string, token?: string) {
13+
async function fetchRepoInfo(repo: string, token?: string, muteLog?: boolean) {
2114
const response = await fetch(`https://api.github.com/repos/${repo}`,
2215
token ? {
2316
headers: {
@@ -28,49 +21,47 @@ async function fetchRepoInfo(repo: string, token?: string) {
2821

2922
switch (response.status) {
3023
case 401:
31-
console.log('⚠ The token provided is invalid or has been revoked.', { token: token });
24+
if (!muteLog) console.log('⚠ The token provided is invalid or has been revoked.', { token: token });
3225
throw new Error('Invalid token');
3326

3427
case 403:
3528
// See https://developer.github.com/v3/#rate-limiting
3629
if (response.headers.get('X-RateLimit-Remaining') === '0') {
37-
console.log('⚠ Your token rate limit has been exceeded.', { token: token });
30+
if (!muteLog) console.log('⚠ Your token rate limit has been exceeded.', { token: token });
3831
throw new Error('Rate limit exceeded');
3932
}
4033

4134
break;
4235

4336
case 404:
44-
console.log('⚠ Repository was not found.', { repo });
37+
if (!muteLog) console.log('⚠ Repository was not found.', { repo });
4538
throw new Error('Repository not found');
4639

4740
default:
4841
}
4942

5043
if (!response.ok) {
51-
console.log('⚠ Could not obtain repository data from the GitHub API.', { repo, response });
44+
if (!muteLog) console.log('⚠ Could not obtain repository data from the GitHub API.', { repo, response });
5245
throw new Error('Fetch error');
5346
}
5447

5548
return response.json();
5649
}
5750

58-
59-
// Great for downloads with many sub directories
60-
// Pros: one request + maybe doesn't require token
61-
// Cons: huge on huge repos + may be truncated
6251
async function viaTreesApi({
6352
user,
6453
repository,
6554
ref = 'HEAD',
6655
directory,
6756
token,
57+
muteLog
6858
}: {
6959
user: string;
7060
repository: string;
7161
ref: string;
7262
directory: string;
7363
token?: string;
64+
muteLog?: boolean;
7465
}) {
7566
if (!directory.endsWith('/')) {
7667
directory += '/';
@@ -84,7 +75,7 @@ async function viaTreesApi({
8475
tree: TreeItem[];
8576
message?: string;
8677
truncated: boolean;
87-
} = await fetchRepoInfo(`${user}/${repository}/git/trees/${ref}?recursive=1`, token);
78+
} = await fetchRepoInfo(`${user}/${repository}/git/trees/${ref}?recursive=1`, token, muteLog);
8879

8980
if (contents.message) {
9081
throw new Error(contents.message);
@@ -99,40 +90,66 @@ async function viaTreesApi({
9990
return files;
10091
}
10192

93+
async function getRepoMeta(user: string, repository: string, ref: string, dir: string, config?: Config) {
10294

103-
export default async function download(source: string, saveTo: string, config?: {
104-
/** JWT token for authorization in private repositories */
105-
token?: string;
95+
const repoIsPrivate: boolean = (await fetchRepoInfo(`${user}/${repository}`, config?.token, config?.muteLog)).private;
10696

107-
/** Max number of async requests at the same time. 10 by default.
108-
* download-directory.github.io has no limit, but it can lead to IP blocking
109-
*/
110-
requests?: number;
111-
}) {
97+
const files: TreeItem[] = await viaTreesApi({
98+
user,
99+
repository,
100+
ref,
101+
directory: decodeURIComponent(dir),
102+
token: config?.token,
103+
muteLog: config?.muteLog,
104+
});
105+
106+
return {
107+
files,
108+
repoIsPrivate
109+
}
110+
}
111+
112+
113+
export default async function download(source: string, saveTo: string, config?: Config): Promise<Stats> {
114+
115+
const stats: Stats = { files: {}, downloaded: 0, success: false };
112116

113117
const [, user, repository, ref, dir] = urlParserRegex.exec(new URL(source).pathname) ?? [];
114118

115119
if (!user || !repository) {
116-
console.error('Invalid url. It must match: ', urlParserRegex);
117-
return;
120+
if (!config?.muteLog) console.error('Invalid url. It must match: ', urlParserRegex);
121+
stats.error = 'Invalid url';
122+
return stats;
118123
}
119124

120-
const { private: repoIsPrivate } = await fetchRepoInfo(`${user}/${repository}`, config?.token);
121125

122-
const files = await viaTreesApi({
123-
user,
124-
repository,
125-
ref,
126-
directory: decodeURIComponent(dir),
127-
token: config?.token,
128-
});
126+
let meta;
127+
try {
128+
meta = await getRepoMeta(user, repository, ref, dir, config)
129+
} catch (e) {
130+
if (!config?.muteLog) console.error('Failed to fetch repo meta info: ', e);
131+
132+
await new Promise(resolve => setTimeout(resolve, 3000));
133+
134+
try {
135+
meta = await getRepoMeta(user, repository, ref, dir, config)
136+
} catch (e) {
137+
if (!config?.muteLog) console.error('Failed to fetch repo meta info after second attempt: ', e);
138+
139+
stats.error = e;
140+
return stats;
141+
}
142+
}
143+
144+
const { files, repoIsPrivate } = meta;
129145

130146
if (files.length === 0) {
131-
console.log('No files to download');
132-
return;
147+
if (!config?.muteLog) console.log('No files to download');
148+
stats.success = true;
149+
return stats;
133150
}
134151

135-
console.log(`Downloading ${files.length} files…`);
152+
if (!config?.muteLog) console.log(`Downloading ${files.length} files…`);
136153

137154

138155
const fetchPublicFile = async (file: TreeItem) => {
@@ -161,24 +178,22 @@ export default async function download(source: string, saveTo: string, config?:
161178
};
162179

163180
let downloaded = 0;
164-
const stats: {
165-
files: Record<string, string>;
166-
downloaded: number;
167-
success: boolean;
168-
} = { files: {}, downloaded: 0, success: false };
169181

170182
const download = async (file: TreeItem) => {
171183
let response;
172184
try {
173185
response = repoIsPrivate ? await fetchPrivateFile(file) :
174186
await fetchPublicFile(file);
175187
} catch (e) {
176-
console.log('⚠ Failed to download file: ' + file.path, e);
188+
if (!config?.muteLog) console.log('⚠ Failed to download file: ' + file.path, e);
189+
190+
await new Promise(resolve => setTimeout(resolve, 2000));
191+
177192
try {
178193
response = repoIsPrivate ? await fetchPrivateFile(file) :
179194
await fetchPublicFile(file);
180195
} catch (e) {
181-
console.log('⚠ Failed to download file after second attempt: ' + file.path, e);
196+
if (!config?.muteLog) console.log('⚠ Failed to download file after second attempt: ' + file.path, e);
182197
return;
183198
}
184199
}
@@ -194,7 +209,7 @@ export default async function download(source: string, saveTo: string, config?:
194209
stats.files[file.path] = fileName;
195210

196211
} catch (e) {
197-
console.error('Failed to write file: ' + file.path, e);
212+
if (!config?.muteLog) console.error('Failed to write file: ' + file.path, e);
198213
}
199214
};
200215

@@ -212,7 +227,7 @@ export default async function download(source: string, saveTo: string, config?:
212227
await Promise.all(statuses);
213228

214229

215-
console.log(`Downloaded ${downloaded}/${files.length} files`);
230+
if (!config?.muteLog) console.log(`Downloaded ${downloaded}/${files.length} files`);
216231

217232
stats.downloaded = downloaded;
218233

src/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export type Config = {
2+
/** JWT token for authorization in private repositories */
3+
token?: string;
4+
5+
/** Max number of async requests at the same time. 10 by default.
6+
* download-directory.github.io has no limit, but it can lead to IP blocking
7+
*/
8+
requests?: number;
9+
10+
/** Disable console logs */
11+
muteLog?: boolean;
12+
}
13+
14+
export type TreeItem = {
15+
path: string;
16+
mode: string;
17+
type: string;
18+
sha: string;
19+
size: number;
20+
url: string;
21+
}
22+
23+
export type Stats = {
24+
files: Record<string, string>;
25+
downloaded: number;
26+
success: boolean;
27+
error?: any;
28+
}

0 commit comments

Comments
 (0)