@@ -39,83 +39,39 @@ function sleep(ms) {
3939 return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
4040}
4141
42- function resolveRedirects ( url , timeout ) {
43- return new Promise ( ( resolve , reject ) => {
44- let redirectCount = 0 ;
45-
46- const follow = ( currentUrl ) => {
47- if ( redirectCount > MAX_REDIRECTS ) {
48- reject ( Object . assign ( new Error ( "Too many redirects" ) , { isHttpError : true } ) ) ;
49- return ;
50- }
51-
52- const client = currentUrl . startsWith ( "https" ) ? https : http ;
53- const parsed = new URL ( currentUrl ) ;
54- const req = client . request ( {
55- method : "HEAD" ,
56- hostname : parsed . hostname ,
57- port : parsed . port ,
58- path : parsed . pathname + parsed . search ,
59- timeout,
60- headers : { "User-Agent" : USER_AGENT } ,
61- } ) ;
62-
63- req . on ( "response" , ( res ) => {
64- res . resume ( ) ;
65- if (
66- res . statusCode === 301 ||
67- res . statusCode === 302 ||
68- res . statusCode === 303 ||
69- res . statusCode === 307 ||
70- res . statusCode === 308
71- ) {
72- const location = res . headers . location ;
73- if ( ! location ) {
74- reject (
75- Object . assign ( new Error ( "Redirect without location header" ) , { isHttpError : true } )
76- ) ;
77- return ;
78- }
79- redirectCount ++ ;
80- follow ( location ) ;
81- return ;
82- }
83- resolve ( { finalUrl : currentUrl , statusCode : res . statusCode } ) ;
84- } ) ;
85-
86- req . on ( "error" , reject ) ;
87- req . on ( "timeout" , ( ) => {
88- req . destroy ( ) ;
89- reject ( Object . assign ( new Error ( "Timeout resolving redirects" ) , { code : "ETIMEDOUT" } ) ) ;
90- } ) ;
91-
92- req . end ( ) ;
93- } ;
94-
95- follow ( url ) ;
96- } ) ;
97- }
42+ function downloadAttempt ( url , tempPath , options ) {
43+ const {
44+ timeout,
45+ onProgress,
46+ signal,
47+ startOffset = 0 ,
48+ expectedSize = 0 ,
49+ _redirects = 0 ,
50+ } = options ;
9851
99- function downloadAttempt ( url , tempPath , { timeout, onProgress, signal, startOffset } ) {
10052 return new Promise ( ( resolve , reject ) => {
10153 if ( signal ?. aborted ) {
10254 reject ( Object . assign ( new Error ( "Download cancelled" ) , { isAbort : true } ) ) ;
10355 return ;
10456 }
10557
58+ if ( _redirects > MAX_REDIRECTS ) {
59+ reject ( Object . assign ( new Error ( "Too many redirects" ) , { isHttpError : true } ) ) ;
60+ return ;
61+ }
62+
10663 const headers = { "User-Agent" : USER_AGENT } ;
10764 if ( startOffset > 0 ) {
10865 headers [ "Range" ] = `bytes=${ startOffset } -` ;
10966 }
11067
11168 const client = url . startsWith ( "https" ) ? https : http ;
112- let activeFile = fs . createWriteStream ( tempPath , { flags : startOffset > 0 ? "a" : "w" } ) ;
113-
69+ let request = null ;
70+ let activeFile = null ;
71+ let stallTimer = null ;
11472 let downloadedSize = startOffset ;
11573 let totalSize = 0 ;
11674 let lastProgressUpdate = 0 ;
117- let request = null ;
118- let stallTimer = null ;
11975
12076 const cleanup = ( ) => {
12177 if ( stallTimer ) {
@@ -126,7 +82,10 @@ function downloadAttempt(url, tempPath, { timeout, onProgress, signal, startOffs
12682 request . destroy ( ) ;
12783 request = null ;
12884 }
129- activeFile . destroy ( ) ;
85+ if ( activeFile ) {
86+ activeFile . destroy ( ) ;
87+ activeFile = null ;
88+ }
13089 } ;
13190
13291 const onAbort = ( ) => {
@@ -140,20 +99,44 @@ function downloadAttempt(url, tempPath, { timeout, onProgress, signal, startOffs
14099
141100 request = client . get ( url , { headers, timeout } , ( response ) => {
142101 if ( signal ?. aborted ) {
102+ response . resume ( ) ;
143103 cleanup ( ) ;
144104 reject ( Object . assign ( new Error ( "Download cancelled" ) , { isAbort : true } ) ) ;
145105 return ;
146106 }
147107
148108 const statusCode = response . statusCode ;
149109
110+ // Follow redirects inline — no separate HEAD resolve step needed
111+ if ( statusCode >= 300 && statusCode < 400 ) {
112+ response . resume ( ) ;
113+ if ( signal ) signal . onAbort = null ;
114+ if ( request ) {
115+ request . destroy ( ) ;
116+ request = null ;
117+ }
118+ const location = response . headers . location ;
119+ if ( ! location ) {
120+ reject (
121+ Object . assign ( new Error ( "Redirect without location header" ) , { isHttpError : true } )
122+ ) ;
123+ return ;
124+ }
125+ downloadAttempt ( location , tempPath , { ...options , _redirects : _redirects + 1 } ) . then (
126+ resolve ,
127+ reject
128+ ) ;
129+ return ;
130+ }
131+
132+ // Content response — create write stream
150133 if ( statusCode === 200 && startOffset > 0 ) {
151134 // Server doesn't support Range — restart from beginning
152135 downloadedSize = 0 ;
153- activeFile . destroy ( ) ;
154136 activeFile = fs . createWriteStream ( tempPath , { flags : "w" } ) ;
155137 totalSize = parseInt ( response . headers [ "content-length" ] , 10 ) || 0 ;
156138 } else if ( statusCode === 206 ) {
139+ activeFile = fs . createWriteStream ( tempPath , { flags : "a" } ) ;
157140 const contentRange = response . headers [ "content-range" ] ;
158141 if ( contentRange ) {
159142 const match = contentRange . match ( / \/ ( \d + ) $ / ) ;
@@ -164,8 +147,10 @@ function downloadAttempt(url, tempPath, { timeout, onProgress, signal, startOffs
164147 totalSize = startOffset + contentLength ;
165148 }
166149 } else if ( statusCode === 200 ) {
150+ activeFile = fs . createWriteStream ( tempPath , { flags : "w" } ) ;
167151 totalSize = parseInt ( response . headers [ "content-length" ] , 10 ) || 0 ;
168152 } else {
153+ response . resume ( ) ;
169154 cleanup ( ) ;
170155 const err = new Error ( `HTTP ${ statusCode } ` ) ;
171156 err . isHttpError = true ;
@@ -174,6 +159,11 @@ function downloadAttempt(url, tempPath, { timeout, onProgress, signal, startOffs
174159 return ;
175160 }
176161
162+ // Fall back to caller-provided expected size when Content-Length is missing
163+ if ( totalSize <= 0 && expectedSize > 0 ) {
164+ totalSize = expectedSize ;
165+ }
166+
177167 const resetStallTimer = ( ) => {
178168 if ( stallTimer ) clearTimeout ( stallTimer ) ;
179169 stallTimer = setTimeout ( ( ) => {
@@ -240,9 +230,12 @@ function downloadAttempt(url, tempPath, { timeout, onProgress, signal, startOffs
240230 } ) ;
241231
242232 function emitProgress ( ) {
243- if ( ! onProgress || totalSize <= 0 ) return ;
233+ if ( ! onProgress ) return ;
244234 const now = Date . now ( ) ;
245- if ( now - lastProgressUpdate >= PROGRESS_THROTTLE_MS || downloadedSize >= totalSize ) {
235+ if (
236+ now - lastProgressUpdate >= PROGRESS_THROTTLE_MS ||
237+ ( totalSize > 0 && downloadedSize >= totalSize )
238+ ) {
246239 lastProgressUpdate = now ;
247240 onProgress ( downloadedSize , totalSize ) ;
248241 }
@@ -256,6 +249,7 @@ async function downloadFile(url, destPath, options = {}) {
256249 timeout = DEFAULT_TIMEOUT ,
257250 maxRetries = DEFAULT_MAX_RETRIES ,
258251 signal,
252+ expectedSize = 0 ,
259253 } = options ;
260254
261255 const tempPath = `${ destPath } .tmp` ;
@@ -273,8 +267,6 @@ async function downloadFile(url, destPath, options = {}) {
273267 // No existing temp file
274268 }
275269
276- const { finalUrl } = await resolveRedirects ( url , timeout ) ;
277-
278270 let lastError = null ;
279271
280272 for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
@@ -297,7 +289,13 @@ async function downloadFile(url, destPath, options = {}) {
297289 }
298290
299291 try {
300- await downloadAttempt ( finalUrl , tempPath , { timeout, onProgress, signal, startOffset } ) ;
292+ await downloadAttempt ( url , tempPath , {
293+ timeout,
294+ onProgress,
295+ signal,
296+ startOffset,
297+ expectedSize,
298+ } ) ;
301299
302300 // Atomic move to final path
303301 try {
0 commit comments