@@ -3,21 +3,14 @@ import fetch from 'node-fetch';
3
3
import { dirname , resolve } from 'path' ;
4
4
import { promisify } from 'util' ;
5
5
6
- const streamPipeline = promisify ( require ( 'stream' ) . pipeline ) ;
6
+ import { Config , Stats , TreeItem } from './types' ;
7
7
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 ) ;
16
9
17
10
// Matches '/<re/po>/tree/<ref>/<dir>'
18
11
const urlParserRegex = / ^ [ / ] ( [ ^ / ] + ) [ / ] ( [ ^ / ] + ) [ / ] t r e e [ / ] ( [ ^ / ] + ) [ / ] ( .* ) / ;
19
12
20
- async function fetchRepoInfo ( repo : string , token ?: string ) {
13
+ async function fetchRepoInfo ( repo : string , token ?: string , muteLog ?: boolean ) {
21
14
const response = await fetch ( `https://api.github.com/repos/${ repo } ` ,
22
15
token ? {
23
16
headers : {
@@ -28,49 +21,47 @@ async function fetchRepoInfo(repo: string, token?: string) {
28
21
29
22
switch ( response . status ) {
30
23
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 } ) ;
32
25
throw new Error ( 'Invalid token' ) ;
33
26
34
27
case 403 :
35
28
// See https://developer.github.com/v3/#rate-limiting
36
29
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 } ) ;
38
31
throw new Error ( 'Rate limit exceeded' ) ;
39
32
}
40
33
41
34
break ;
42
35
43
36
case 404 :
44
- console . log ( '⚠ Repository was not found.' , { repo } ) ;
37
+ if ( ! muteLog ) console . log ( '⚠ Repository was not found.' , { repo } ) ;
45
38
throw new Error ( 'Repository not found' ) ;
46
39
47
40
default :
48
41
}
49
42
50
43
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 } ) ;
52
45
throw new Error ( 'Fetch error' ) ;
53
46
}
54
47
55
48
return response . json ( ) ;
56
49
}
57
50
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
62
51
async function viaTreesApi ( {
63
52
user,
64
53
repository,
65
54
ref = 'HEAD' ,
66
55
directory,
67
56
token,
57
+ muteLog
68
58
} : {
69
59
user : string ;
70
60
repository : string ;
71
61
ref : string ;
72
62
directory : string ;
73
63
token ?: string ;
64
+ muteLog ?: boolean ;
74
65
} ) {
75
66
if ( ! directory . endsWith ( '/' ) ) {
76
67
directory += '/' ;
@@ -84,7 +75,7 @@ async function viaTreesApi({
84
75
tree : TreeItem [ ] ;
85
76
message ?: string ;
86
77
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 ) ;
88
79
89
80
if ( contents . message ) {
90
81
throw new Error ( contents . message ) ;
@@ -99,40 +90,66 @@ async function viaTreesApi({
99
90
return files ;
100
91
}
101
92
93
+ async function getRepoMeta ( user : string , repository : string , ref : string , dir : string , config ?: Config ) {
102
94
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 ;
106
96
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 } ;
112
116
113
117
const [ , user , repository , ref , dir ] = urlParserRegex . exec ( new URL ( source ) . pathname ) ?? [ ] ;
114
118
115
119
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 ;
118
123
}
119
124
120
- const { private : repoIsPrivate } = await fetchRepoInfo ( `${ user } /${ repository } ` , config ?. token ) ;
121
125
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 ;
129
145
130
146
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 ;
133
150
}
134
151
135
- console . log ( `Downloading ${ files . length } files…` ) ;
152
+ if ( ! config ?. muteLog ) console . log ( `Downloading ${ files . length } files…` ) ;
136
153
137
154
138
155
const fetchPublicFile = async ( file : TreeItem ) => {
@@ -161,24 +178,22 @@ export default async function download(source: string, saveTo: string, config?:
161
178
} ;
162
179
163
180
let downloaded = 0 ;
164
- const stats : {
165
- files : Record < string , string > ;
166
- downloaded : number ;
167
- success : boolean ;
168
- } = { files : { } , downloaded : 0 , success : false } ;
169
181
170
182
const download = async ( file : TreeItem ) => {
171
183
let response ;
172
184
try {
173
185
response = repoIsPrivate ? await fetchPrivateFile ( file ) :
174
186
await fetchPublicFile ( file ) ;
175
187
} 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
+
177
192
try {
178
193
response = repoIsPrivate ? await fetchPrivateFile ( file ) :
179
194
await fetchPublicFile ( file ) ;
180
195
} 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 ) ;
182
197
return ;
183
198
}
184
199
}
@@ -194,7 +209,7 @@ export default async function download(source: string, saveTo: string, config?:
194
209
stats . files [ file . path ] = fileName ;
195
210
196
211
} 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 ) ;
198
213
}
199
214
} ;
200
215
@@ -212,7 +227,7 @@ export default async function download(source: string, saveTo: string, config?:
212
227
await Promise . all ( statuses ) ;
213
228
214
229
215
- console . log ( `Downloaded ${ downloaded } /${ files . length } files` ) ;
230
+ if ( ! config ?. muteLog ) console . log ( `Downloaded ${ downloaded } /${ files . length } files` ) ;
216
231
217
232
stats . downloaded = downloaded ;
218
233
0 commit comments