1
- import got from "got"
1
+ import got , { Headers , Options } from "got"
2
2
import fileType from "file-type"
3
3
import path from "path"
4
- import { IncomingMessage , OutgoingHttpHeaders } from "http"
5
4
import fs from "fs-extra"
6
5
import { createContentDigest } from "./create-content-digest"
7
6
import {
@@ -10,7 +9,8 @@ import {
10
9
createFilePath ,
11
10
} from "./filename-utils"
12
11
13
- import { GatsbyCache } from "gatsby"
12
+ import type { IncomingMessage } from "http"
13
+ import type { GatsbyCache } from "gatsby"
14
14
15
15
export interface IFetchRemoteFileOptions {
16
16
url : string
@@ -19,7 +19,7 @@ export interface IFetchRemoteFileOptions {
19
19
htaccess_pass ?: string
20
20
htaccess_user ?: string
21
21
}
22
- httpHeaders ?: OutgoingHttpHeaders
22
+ httpHeaders ?: Headers
23
23
ext ?: string
24
24
name ?: string
25
25
}
@@ -57,10 +57,10 @@ const INCOMPLETE_RETRY_LIMIT = process.env.GATSBY_INCOMPLETE_RETRY_LIMIT
57
57
* @return {Promise<Object> } Resolves with the [http Result Object]{@link https://nodejs.org/api/http.html#http_class_http_serverresponse}
58
58
*/
59
59
const requestRemoteNode = (
60
- url : got . GotUrl ,
61
- headers : OutgoingHttpHeaders ,
60
+ url : string | URL ,
61
+ headers : Headers ,
62
62
tmpFilename : string ,
63
- httpOptions : got . GotOptions < string | null > | undefined ,
63
+ httpOptions ?: Options ,
64
64
attempt : number = 1
65
65
) : Promise < IncomingMessage > =>
66
66
new Promise ( ( resolve , reject ) => {
@@ -71,12 +71,15 @@ const requestRemoteNode = (
71
71
const handleTimeout = async ( ) : Promise < void > => {
72
72
fsWriteStream . close ( )
73
73
fs . removeSync ( tmpFilename )
74
+
74
75
if ( attempt < STALL_RETRY_LIMIT ) {
75
76
// Retry by calling ourself recursively
76
77
resolve (
77
78
requestRemoteNode ( url , headers , tmpFilename , httpOptions , attempt + 1 )
78
79
)
79
80
} else {
81
+ // TODO move to new Error type
82
+ // eslint-disable-next-line prefer-promise-reject-errors
80
83
reject ( `Failed to download ${ url } after ${ STALL_RETRY_LIMIT } attempts` )
81
84
}
82
85
}
@@ -93,15 +96,21 @@ const requestRemoteNode = (
93
96
send : CONNECTION_TIMEOUT , // https://github.com/sindresorhus/got#timeout
94
97
} ,
95
98
...httpOptions ,
99
+ isStream : true ,
96
100
} )
97
101
98
102
let haveAllBytesBeenWritten = false
103
+ // Fixes a bug in latest got where progress.total gets reset when stream ends, even if it wasn't complete.
104
+ let totalSize : number | null = null
99
105
responseStream . on ( `downloadProgress` , progress => {
100
106
if (
101
- progress . transferred === progress . total ||
102
- progress . total === null ||
103
- progress . total === undefined
107
+ progress . total != null &&
108
+ ( ! totalSize || totalSize < progress . total )
104
109
) {
110
+ totalSize = progress . total
111
+ }
112
+
113
+ if ( progress . transferred === totalSize || totalSize === null ) {
105
114
haveAllBytesBeenWritten = true
106
115
}
107
116
} )
@@ -113,29 +122,36 @@ const requestRemoteNode = (
113
122
if ( timeout ) {
114
123
clearTimeout ( timeout )
115
124
}
125
+
126
+ fsWriteStream . close ( )
116
127
fs . removeSync ( tmpFilename )
117
128
reject ( error )
118
129
} )
119
130
120
- fsWriteStream . on ( `error` , ( error : any ) => {
131
+ fsWriteStream . on ( `error` , ( error : unknown ) => {
121
132
if ( timeout ) {
122
133
clearTimeout ( timeout )
123
134
}
135
+
124
136
reject ( error )
125
137
} )
126
138
127
139
responseStream . on ( `response` , response => {
128
140
resetTimeout ( )
129
141
130
142
fsWriteStream . on ( `finish` , ( ) => {
143
+ if ( timeout ) {
144
+ clearTimeout ( timeout )
145
+ }
146
+
131
147
fsWriteStream . close ( )
132
148
133
149
// We have an incomplete download
134
150
if ( ! haveAllBytesBeenWritten ) {
135
151
fs . removeSync ( tmpFilename )
136
152
137
153
if ( attempt < INCOMPLETE_RETRY_LIMIT ) {
138
- resolve (
154
+ return resolve (
139
155
requestRemoteNode (
140
156
url ,
141
157
headers ,
@@ -145,16 +161,15 @@ const requestRemoteNode = (
145
161
)
146
162
)
147
163
} else {
148
- reject (
164
+ // TODO move to new Error type
165
+ // eslint-disable-next-line prefer-promise-reject-errors
166
+ return reject (
149
167
`Failed to download ${ url } after ${ INCOMPLETE_RETRY_LIMIT } attempts`
150
168
)
151
169
}
152
170
}
153
171
154
- if ( timeout ) {
155
- clearTimeout ( timeout )
156
- }
157
- resolve ( response )
172
+ return resolve ( response )
158
173
} )
159
174
} )
160
175
} )
@@ -177,14 +192,11 @@ export async function fetchRemoteFile({
177
192
headers [ `If-None-Match` ] = cachedHeaders . etag
178
193
}
179
194
180
- // Add Basic authentication if passed in:
181
- // https://github.com/sindresorhus/got/blob/main/documentation/2-options.md#username
182
- // The "auth" API isn't particularly extensible, we should define an API that we validate
183
- const httpOptions : got . GotOptions < string | null > = { }
195
+ // Add htaccess authentication if passed in. This isn't particularly
196
+ // extensible. We should define a proper API that we validate.
197
+ const httpOptions : Options = { }
184
198
if ( auth && ( auth . htaccess_pass || auth . htaccess_user ) ) {
185
- // @ts -ignore - We use outdated @types/got typings. Once we update got everywhere we can remove @types/got and have correct typings
186
199
httpOptions . username = auth . htaccess_user
187
- // @ts -ignore - see above
188
200
httpOptions . password = auth . htaccess_pass
189
201
}
190
202
0 commit comments