Downloader with resume capability for large files.
| 6 | |
| 7 | |
| 8 | class ResumableDownloader: |
| 9 | """Downloader with resume capability for large files.""" |
| 10 | |
| 11 | def __init__(self, chunk_size: int = 8192, max_retries: int = 5): |
| 12 | self.chunk_size = chunk_size |
| 13 | self.max_retries = max_retries |
| 14 | |
| 15 | def download(self, url: str, file_path: Path) -> None: |
| 16 | """Download with resume capability. |
| 17 | |
| 18 | Args: |
| 19 | url: URL to download |
| 20 | file_path: Path where to save the file |
| 21 | """ |
| 22 | import time |
| 23 | import urllib.error |
| 24 | import urllib.request |
| 25 | |
| 26 | # Get the total file size |
| 27 | total_size = self._get_file_size(url) |
| 28 | if total_size is None: |
| 29 | print(f"WARNING: Could not determine file size for {url}") |
| 30 | total_size = 0 |
| 31 | else: |
| 32 | print( |
| 33 | f"File size: {total_size:,} bytes ({total_size / (1024 * 1024):.1f} MB)" |
| 34 | ) |
| 35 | |
| 36 | # Check if partial file exists |
| 37 | start_byte = 0 |
| 38 | if file_path.exists(): |
| 39 | start_byte = file_path.stat().st_size |
| 40 | if start_byte == total_size: |
| 41 | print(f"File already completely downloaded: {file_path}") |
| 42 | return |
| 43 | elif start_byte > 0: |
| 44 | print( |
| 45 | f"Resuming download from byte {start_byte:,} ({start_byte / (1024 * 1024):.1f} MB)" |
| 46 | ) |
| 47 | |
| 48 | retry_count = 0 |
| 49 | while retry_count <= self.max_retries: |
| 50 | try: |
| 51 | self._download_range(url, file_path, start_byte, total_size) |
| 52 | print(f"SUCCESS: Download completed successfully: {file_path}") |
| 53 | return |
| 54 | except (urllib.error.URLError, ConnectionError, OSError): |
| 55 | retry_count += 1 |
| 56 | current_size = file_path.stat().st_size if file_path.exists() else 0 |
| 57 | |
| 58 | if retry_count <= self.max_retries: |
| 59 | wait_time = min(2**retry_count, 30) # Exponential backoff, max 30s |
| 60 | print( |
| 61 | f"\nCONNECTION LOST: At {current_size:,} bytes. Retry {retry_count}/{self.max_retries} in {wait_time}s..." |
| 62 | ) |
| 63 | time.sleep(wait_time) |
| 64 | start_byte = current_size |
| 65 | else: |
no outgoing calls
no test coverage detected