@@ -20,6 +20,12 @@ const BUFFER_SIZE: usize = 8192;
2020/// File size threshold for showing hash progress bar (2MB)
2121const LARGE_FILE_THRESHOLD : u64 = 2 * 1024 * 1024 ;
2222
23+ /// Maximum number of retry attempts for failed downloads
24+ const MAX_RETRIES : u32 = 3 ;
25+
26+ /// Initial delay between retries in milliseconds (doubles with each retry)
27+ const INITIAL_RETRY_DELAY_MS : u64 = 1000 ;
28+
2329/// Sets up signal handling for graceful shutdown on Ctrl+C
2430///
2531/// Returns an Arc<AtomicBool> that can be checked to see if the process
@@ -153,101 +159,202 @@ fn prepare_file_for_download(file_path: &str) -> Result<File> {
153159 Ok ( file)
154160}
155161
156- /// Download file content with progress reporting
162+ /// Download file content with progress reporting and automatic retry on failure
157163async fn download_file_content (
158164 client : & Client ,
159165 url : & str ,
160- file_size : u64 ,
161166 file : & mut File ,
162167 running : & Arc < AtomicBool > ,
163- is_resuming : bool ,
164168) -> Result < u64 > {
165- let download_action = if is_resuming {
166- format ! ( "{} {} " , "╰╼" . cyan( ) . dimmed( ) , "Resuming" . white( ) )
167- } else {
168- format ! ( "{} {} " , "╰╼" . cyan( ) . dimmed( ) , "Downloading" . white( ) )
169- } ;
170-
171- let mut headers = HeaderMap :: new ( ) ;
172- if file_size > 0 {
173- // Use IaGetError::Network for header parsing errors
174- headers. insert (
175- reqwest:: header:: RANGE ,
176- HeaderValue :: from_str ( & format ! ( "bytes={}-" , file_size) )
177- . map_err ( |e| IaGetError :: Network ( format ! ( "Invalid range header value: {}" , e) ) ) ?,
178- ) ;
179- }
169+ let mut retry_count = 0 ;
180170
181- let mut response = if file_size > 0 && is_resuming {
182- // Ensure headers are only used for resume
183- client. get ( url) . headers ( headers) . send ( ) . await ?
184- } else {
185- client. get ( url) . send ( ) . await ?
186- } ;
171+ loop {
172+ // Re-check file size at start of each attempt (in case of retry)
173+ let current_file_size = file. metadata ( ) ?. len ( ) ;
174+ let download_action = if current_file_size > 0 {
175+ format ! ( "{} {} " , "╰╼" . cyan( ) . dimmed( ) , "Resuming" . white( ) )
176+ } else {
177+ format ! ( "{} {} " , "╰╼" . cyan( ) . dimmed( ) , "Downloading" . white( ) )
178+ } ;
179+
180+ let mut headers = HeaderMap :: new ( ) ;
181+ if current_file_size > 0 {
182+ // Use IaGetError::Network for header parsing errors
183+ headers. insert (
184+ reqwest:: header:: RANGE ,
185+ HeaderValue :: from_str ( & format ! ( "bytes={}-" , current_file_size) ) . map_err ( |e| {
186+ IaGetError :: Network ( format ! ( "Invalid range header value: {}" , e) )
187+ } ) ?,
188+ ) ;
189+ }
187190
188- let content_length = response. content_length ( ) . unwrap_or ( 0 ) ;
189- let total_expected_size = if is_resuming {
190- content_length + file_size
191- } else {
192- content_length
193- } ;
191+ // Try to send the request with retry logic
192+ let mut response = match if current_file_size > 0 {
193+ client. get ( url) . headers ( headers) . send ( ) . await
194+ } else {
195+ client. get ( url) . send ( ) . await
196+ } {
197+ Ok ( resp) => resp,
198+ Err ( e) => {
199+ // Request failed before we even got a response
200+ retry_count += 1 ;
201+
202+ if retry_count > MAX_RETRIES {
203+ println ! (
204+ "{} {} {} Maximum retries ({}) exceeded" ,
205+ "├╼" . cyan( ) . dimmed( ) ,
206+ "Failed" . red( ) . bold( ) ,
207+ "✘" . red( ) . bold( ) ,
208+ MAX_RETRIES
209+ ) ;
210+ return Err ( e. into ( ) ) ;
211+ }
212+
213+ let delay = INITIAL_RETRY_DELAY_MS * 2u64 . pow ( retry_count - 1 ) ;
214+ println ! (
215+ "{} {} {} Connection error (attempt {}/{}): {}" ,
216+ "├╼" . cyan( ) . dimmed( ) ,
217+ "Retry" . yellow( ) . bold( ) ,
218+ "⟳" . yellow( ) . bold( ) ,
219+ retry_count,
220+ MAX_RETRIES ,
221+ e
222+ ) ;
223+ println ! (
224+ "{} {} Waiting {:.1}s before retry..." ,
225+ "├╼" . cyan( ) . dimmed( ) ,
226+ "Wait" . white( ) ,
227+ delay as f64 / 1000.0
228+ ) ;
194229
195- let pb = create_progress_bar (
196- total_expected_size,
197- & download_action,
198- Some ( "green/green" ) , // Color for download bar
199- true ,
200- ) ;
230+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( delay) ) . await ;
201231
202- // Set initial progress to current file size for resumed downloads
203- pb. set_position ( file_size) ;
232+ // Ensure file is ready for next attempt
233+ file. flush ( ) ?;
234+ file. seek ( SeekFrom :: End ( 0 ) ) ?;
204235
205- let start_time = std:: time:: Instant :: now ( ) ;
206- let mut total_bytes: u64 = file_size;
207- let mut downloaded_bytes: u64 = 0 ;
236+ continue ; // Retry from the top of the loop
237+ }
238+ } ;
239+
240+ let content_length = response. content_length ( ) . unwrap_or ( 0 ) ;
241+ let total_expected_size = if current_file_size > 0 {
242+ content_length + current_file_size
243+ } else {
244+ content_length
245+ } ;
246+
247+ let pb = create_progress_bar (
248+ total_expected_size,
249+ & download_action,
250+ Some ( "green/green" ) ,
251+ true ,
252+ ) ;
208253
209- while let Some ( chunk) = response. chunk ( ) . await ? {
210- if !running. load ( Ordering :: SeqCst ) {
211- pb. finish_and_clear ( ) ;
212- return Err ( std:: io:: Error :: new (
213- std:: io:: ErrorKind :: Interrupted ,
214- "Download interrupted during file transfer" ,
215- )
216- . into ( ) ) ;
254+ // Set initial progress to current file size for resumed downloads
255+ pb. set_position ( current_file_size) ;
256+
257+ let start_time = std:: time:: Instant :: now ( ) ;
258+ let mut total_bytes: u64 = current_file_size;
259+ let mut downloaded_bytes: u64 = 0 ;
260+
261+ // Attempt the download
262+ let download_result: Result < ( ) > = async {
263+ while let Some ( chunk_result) = response. chunk ( ) . await . transpose ( ) {
264+ if !running. load ( Ordering :: SeqCst ) {
265+ pb. finish_and_clear ( ) ;
266+ return Err ( std:: io:: Error :: new (
267+ std:: io:: ErrorKind :: Interrupted ,
268+ "Download interrupted during file transfer" ,
269+ )
270+ . into ( ) ) ;
271+ }
272+
273+ let chunk = chunk_result?;
274+ file. write_all ( & chunk) ?;
275+ downloaded_bytes += chunk. len ( ) as u64 ;
276+ total_bytes += chunk. len ( ) as u64 ;
277+ pb. set_position ( total_bytes) ;
278+ }
279+ Ok ( ( ) )
217280 }
281+ . await ;
218282
219- file. write_all ( & chunk) ?;
220- downloaded_bytes += chunk. len ( ) as u64 ;
221- total_bytes += chunk. len ( ) as u64 ;
222- pb. set_position ( total_bytes) ;
223- }
283+ match download_result {
284+ Ok ( _) => {
285+ // Ensure data is written to disk
286+ file. flush ( ) ?;
224287
225- // Ensure data is written to disk
226- file. flush ( ) ?;
288+ let elapsed = start_time. elapsed ( ) ;
289+ let elapsed_secs = elapsed. as_secs_f64 ( ) ;
290+ let transfer_rate_val = if elapsed_secs > 0.0 {
291+ downloaded_bytes as f64 / elapsed_secs
292+ } else {
293+ 0.0
294+ } ;
227295
228- let elapsed = start_time. elapsed ( ) ;
229- let elapsed_secs = elapsed. as_secs_f64 ( ) ;
230- let transfer_rate_val = if elapsed_secs > 0.0 {
231- downloaded_bytes as f64 / elapsed_secs
232- } else {
233- 0.0
234- } ;
296+ let ( rate, unit) = format_transfer_rate ( transfer_rate_val) ;
297+
298+ pb. finish_and_clear ( ) ;
299+ println ! (
300+ "{} {} {} {} in {} ({:.2} {}/s)" ,
301+ "├╼" . cyan( ) . dimmed( ) ,
302+ "Downloaded" . white( ) ,
303+ "↓" . green( ) . bold( ) ,
304+ format_size( downloaded_bytes) . bold( ) ,
305+ format_duration( elapsed) . bold( ) ,
306+ rate,
307+ unit
308+ ) ;
235309
236- let ( rate, unit) = format_transfer_rate ( transfer_rate_val) ;
237-
238- pb. finish_and_clear ( ) ;
239- println ! (
240- "{} {} {} {} in {} ({:.2} {}/s)" ,
241- "├╼" . cyan( ) . dimmed( ) ,
242- "Downloaded" . white( ) ,
243- "↓" . green( ) . bold( ) ,
244- format_size( downloaded_bytes) . bold( ) ,
245- format_duration( elapsed) . bold( ) ,
246- rate,
247- unit
248- ) ;
249-
250- Ok ( total_bytes)
310+ return Ok ( total_bytes) ;
311+ }
312+ Err ( e) => {
313+ pb. finish_and_clear ( ) ;
314+
315+ // Check if this is a user interruption
316+ if e. to_string ( ) . contains ( "interrupted" ) {
317+ return Err ( e) ;
318+ }
319+
320+ retry_count += 1 ;
321+
322+ if retry_count > MAX_RETRIES {
323+ println ! (
324+ "{} {} {} Maximum retries ({}) exceeded" ,
325+ "├╼" . cyan( ) . dimmed( ) ,
326+ "Failed" . red( ) . bold( ) ,
327+ "✘" . red( ) . bold( ) ,
328+ MAX_RETRIES
329+ ) ;
330+ return Err ( e) ;
331+ }
332+
333+ let delay = INITIAL_RETRY_DELAY_MS * 2u64 . pow ( retry_count - 1 ) ;
334+ println ! (
335+ "{} {} {} Download error (attempt {}/{}): {}" ,
336+ "├╼" . cyan( ) . dimmed( ) ,
337+ "Retry" . yellow( ) . bold( ) ,
338+ "⟳" . yellow( ) . bold( ) ,
339+ retry_count,
340+ MAX_RETRIES ,
341+ e
342+ ) ;
343+ println ! (
344+ "{} {} Waiting {:.1}s before retry..." ,
345+ "├╼" . cyan( ) . dimmed( ) ,
346+ "Wait" . white( ) ,
347+ delay as f64 / 1000.0
348+ ) ;
349+
350+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( delay) ) . await ;
351+
352+ // Ensure file is flushed and ready for next attempt
353+ file. flush ( ) ?;
354+ file. seek ( SeekFrom :: End ( 0 ) ) ?;
355+ }
356+ }
357+ }
251358}
252359
253360/// Verify a downloaded file's hash against an expected value
@@ -349,9 +456,7 @@ where
349456
350457 let mut file = prepare_file_for_download ( & file_path) ?;
351458
352- let file_size = file. metadata ( ) ?. len ( ) ;
353- let is_resuming = file_size > 0 ;
354- download_file_content ( client, & url, file_size, & mut file, & running, is_resuming) . await ?;
459+ download_file_content ( client, & url, & mut file, & running) . await ?;
355460 verify_downloaded_file ( & file_path, expected_md5. as_deref ( ) , & running) ?;
356461 }
357462
0 commit comments