1212import enum
1313import hashlib
1414import atexit
15-
15+ import json
1616from typing import Union
1717from pathlib import Path
1818
@@ -165,17 +165,20 @@ class DownloadObject:
165165
166166 """
167167
168- def __init__ (self , url : str , path : str ,size :str = None ) -> None :
168+ def __init__ (self , url : str , path : str , size :str = None , resume_download : bool = True ) -> None :
169169 self .url : str = url
170170 self .status : str = DownloadStatus .INACTIVE
171171 self .error_msg : str = ""
172172 self .filename : str = self ._get_filename ()
173173 self .size : str = size
174+ self .resume_download : bool = resume_download
174175
175176 self .filepath : Path = Path (path )
177+ self .progress_file : Path = Path (f"{ path } .progress" )
176178
177179 self .total_file_size : float = 0.0
178180 self .downloaded_file_size : float = 0.0
181+ self .downloaded_file_offset : float = 0.0
179182 self .start_time : float = time .time ()
180183
181184 self .error : bool = False
@@ -304,7 +307,7 @@ def _update_checksum(self, chunk: bytes) -> None:
304307
305308 def _prepare_working_directory (self , path : Path ) -> bool :
306309 """
307- Validates working enviroment, including free space and removing existing files
310+ Validates working enviroment, including free space and handling existing files
308311
309312 Parameters:
310313 path (str): Path to the file
@@ -315,8 +318,13 @@ def _prepare_working_directory(self, path: Path) -> bool:
315318
316319 try :
317320 if Path (path ).exists ():
318- logging .info (f"Deleting existing file: { path } " )
319- Path (path ).unlink ()
321+ if self .resume_download :
322+ # For resumable download, keep the existing file
323+ self .downloaded_file_offset = Path (path ).stat ().st_size
324+ logging .info (f"Resuming download from { utilities .human_fmt (self .downloaded_file_offset )} : { path } " )
325+ else :
326+ logging .info (f"Deleting existing file: { path } " )
327+ Path (path ).unlink ()
320328 return True
321329
322330 if not Path (path ).parent .exists ():
@@ -339,10 +347,55 @@ def _prepare_working_directory(self, path: Path) -> bool:
339347 logging .info (f"- Directory ready: { path } " )
340348 return True
341349
350+ def _save_progress (self ) -> None :
351+ """
352+ Save download progress to a file
353+ """
354+ try :
355+ with open (self .progress_file , 'w' ) as f :
356+ json .dump ({
357+ 'downloaded' : self .downloaded_file_size ,
358+ 'total' : self .total_file_size ,
359+ 'offset' : self .downloaded_file_offset
360+ }, f )
361+ except Exception as e :
362+ logging .warning (f"Failed to save download progress: { str (e )} " )
363+
364+ def _load_progress (self ) -> bool :
365+ """
366+ Load download progress from file
367+
368+ Returns:
369+ bool: True if progress was loaded, False otherwise
370+ """
371+ if not self .progress_file .exists ():
372+ return False
373+
374+ try :
375+ with open (self .progress_file , 'r' ) as f :
376+ progress = json .load (f )
377+ self .downloaded_file_size = progress .get ('downloaded' , 0 )
378+ self .total_file_size = progress .get ('total' , 0 )
379+ self .downloaded_file_offset = progress .get ('offset' , 0 )
380+ return True
381+ except Exception as e :
382+ logging .warning (f"Failed to load download progress: { str (e )} " )
383+ return False
384+
385+ def _clear_progress (self ) -> None :
386+ """
387+ Clear download progress file
388+ """
389+ try :
390+ if self .progress_file .exists ():
391+ self .progress_file .unlink ()
392+ except Exception as e :
393+ logging .warning (f"Failed to clear progress file: { str (e )} " )
394+
342395
343396 def _download (self , display_progress : bool = False ) -> None :
344397 """
345- Download the file
398+ Download the file with resumable support
346399
347400 Libraries should invoke download() instead of this method
348401
@@ -359,12 +412,19 @@ def _download(self, display_progress: bool = False) -> None:
359412 if self ._prepare_working_directory (self .filepath ) is False :
360413 raise Exception (self .error_msg )
361414
362- response = NetworkUtilities ().get (self .url , stream = True , timeout = 100 )
415+ headers = {}
416+ if self .resume_download and self .downloaded_file_offset > 0 :
417+ headers ['Range' ] = f'bytes={ self .downloaded_file_offset } -'
418+ logging .info (f"Resuming download from byte { self .downloaded_file_offset } " )
419+
420+ response = NetworkUtilities ().get (self .url , stream = True , timeout = 100 , headers = headers )
363421
364- with open (self .filepath , 'wb' ) as file :
422+ mode = 'ab' if self .resume_download and self .downloaded_file_offset > 0 else 'wb'
423+ with open (self .filepath , mode ) as file :
365424 atexit .register (self .stop )
366425 for i , chunk in enumerate (response .iter_content (1024 * 1024 * 4 )):
367426 if self .should_stop :
427+ self ._save_progress ()
368428 raise Exception ("Download stopped" )
369429
370430 if chunk :
@@ -378,14 +438,20 @@ def _download(self, display_progress: bool = False) -> None:
378438 print (f"Downloaded { utilities .human_fmt (self .downloaded_file_size )} of { self .filename } " )
379439 else :
380440 print (f"Downloaded { self .get_percent ():.2f} % of { self .filename } ({ utilities .human_fmt (self .get_speed ())} /s) ({ self .get_time_remaining ():.2f} seconds remaining)" )
381- self .download_complete = True
382- logging .info (f"Download complete: { self .filename } " )
383- logging .info ("Stats:" )
384- logging .info (f"- Downloaded size: { utilities .human_fmt (self .downloaded_file_size )} " )
385- logging .info (f"- Time elapsed: { (time .time () - self .start_time ):.2f} seconds" )
386- logging .info (f"- Speed: { utilities .human_fmt (self .downloaded_file_size / (time .time () - self .start_time ))} /s" )
387- logging .info (f"- Location: { self .filepath } " )
441+
442+ if response .status_code == 206 : # Partial Content
443+ self ._save_progress ()
444+ else :
445+ self .download_complete = True
446+ self ._clear_progress ()
447+ logging .info (f"Download complete: { self .filename } " )
448+ logging .info ("Stats:" )
449+ logging .info (f"- Downloaded size: { utilities .human_fmt (self .downloaded_file_size )} " )
450+ logging .info (f"- Time elapsed: { (time .time () - self .start_time ):.2f} seconds" )
451+ logging .info (f"- Speed: { utilities .human_fmt (self .downloaded_file_size / (time .time () - self .start_time ))} /s" )
452+ logging .info (f"- Location: { self .filepath } " )
388453 except Exception as e :
454+ self ._save_progress ()
389455 self .error = True
390456 self .error_msg = str (e )
391457 self .status = DownloadStatus .ERROR
0 commit comments