|
37 | 37 | import hashlib |
38 | 38 | import json |
39 | 39 | import os |
| 40 | +import time |
40 | 41 | import sys |
41 | 42 | import tarfile |
42 | 43 | import zipfile |
@@ -195,41 +196,96 @@ def find_in_index(cef_version, cef_postfix2): |
195 | 196 | sys.exit(1) |
196 | 197 |
|
197 | 198 |
|
198 | | -def download(url, dest_path, expected_size=0): |
199 | | - """Download url to dest_path with a progress bar.""" |
| 199 | +def download(url, dest_path, expected_size=0, max_retries=50): |
| 200 | + """Download url to dest_path with a progress bar and resume support.""" |
200 | 201 | try: |
201 | | - from urllib.request import urlopen |
| 202 | + from urllib.request import Request, urlopen |
202 | 203 | except ImportError: |
203 | | - from urllib2 import urlopen |
| 204 | + from urllib2 import Request, urlopen |
204 | 205 |
|
205 | 206 | log("Downloading: {}".format(url)) |
206 | | - try: |
207 | | - response = urlopen(url, timeout=120) |
208 | | - except Exception as exc: |
209 | | - log("ERROR: Download failed: {}".format(exc)) |
210 | | - sys.exit(1) |
211 | 207 |
|
212 | | - total = int(response.headers.get("Content-Length") or expected_size or 0) |
| 208 | + total = expected_size |
213 | 209 | downloaded = 0 |
214 | 210 | chunk_size = 1024 * 1024 # 1 MB |
215 | 211 |
|
216 | | - try: |
217 | | - with open(dest_path, "wb") as fp: |
218 | | - while True: |
219 | | - chunk = response.read(chunk_size) |
220 | | - if not chunk: |
221 | | - break |
222 | | - fp.write(chunk) |
223 | | - downloaded += len(chunk) |
224 | | - _print_progress(downloaded, total) |
225 | | - except Exception as exc: |
226 | | - if os.path.isfile(dest_path): |
227 | | - os.remove(dest_path) |
228 | | - log("\nERROR: Download failed: {}".format(exc)) |
229 | | - sys.exit(1) |
| 212 | + # Resume from an existing partial file if present |
| 213 | + if os.path.isfile(dest_path): |
| 214 | + downloaded = os.path.getsize(dest_path) |
| 215 | + if total and downloaded >= total: |
| 216 | + log("Already fully downloaded: {}".format(os.path.basename(dest_path))) |
| 217 | + return |
| 218 | + if downloaded > 0: |
| 219 | + log("Resuming from {:.1f} MB...".format(downloaded / (1024 * 1024))) |
| 220 | + |
| 221 | + for attempt in range(1, max_retries + 1): |
| 222 | + if attempt > 1: |
| 223 | + # Re-check how much we have on disk after a failed attempt |
| 224 | + if os.path.isfile(dest_path): |
| 225 | + downloaded = os.path.getsize(dest_path) |
| 226 | + log("Retry {}/{} (resuming from {:.1f} MB): {}".format( |
| 227 | + attempt, max_retries, downloaded / (1024 * 1024), url)) |
| 228 | + |
| 229 | + req = Request(url) |
| 230 | + if downloaded > 0: |
| 231 | + req.add_header("Range", "bytes={}-".format(downloaded)) |
| 232 | + |
| 233 | + try: |
| 234 | + response = urlopen(req, timeout=120) |
| 235 | + except Exception as exc: |
| 236 | + if attempt == max_retries: |
| 237 | + log("ERROR: Download failed: {}".format(exc)) |
| 238 | + sys.exit(1) |
| 239 | + log("WARNING: Connection error (attempt {}): {}".format(attempt, exc)) |
| 240 | + time.sleep(2) |
| 241 | + continue |
230 | 242 |
|
231 | | - print() # end progress line |
232 | | - log("Saved: {}".format(os.path.basename(dest_path))) |
| 243 | + # Determine total size from response |
| 244 | + content_range = response.headers.get("Content-Range") |
| 245 | + if content_range: |
| 246 | + # e.g. "bytes 100-200/614600000" |
| 247 | + try: |
| 248 | + total = int(content_range.rsplit("/", 1)[-1]) |
| 249 | + except (ValueError, IndexError): |
| 250 | + pass |
| 251 | + else: |
| 252 | + total = int(response.headers.get("Content-Length") or total or 0) |
| 253 | + if downloaded > 0: |
| 254 | + # Server didn't honour Range; start over |
| 255 | + log("WARNING: Server ignored Range header, restarting download.") |
| 256 | + downloaded = 0 |
| 257 | + |
| 258 | + error = None |
| 259 | + |
| 260 | + try: |
| 261 | + mode = "ab" if downloaded > 0 else "wb" |
| 262 | + with open(dest_path, mode) as fp: |
| 263 | + while True: |
| 264 | + chunk = response.read(chunk_size) |
| 265 | + if not chunk: |
| 266 | + break |
| 267 | + fp.write(chunk) |
| 268 | + downloaded += len(chunk) |
| 269 | + _print_progress(downloaded, total) |
| 270 | + except Exception as exc: |
| 271 | + error = exc |
| 272 | + |
| 273 | + print() # end progress line |
| 274 | + |
| 275 | + if error is None and total and downloaded < total: |
| 276 | + error = "short read: got {:.1f} MB of {:.1f} MB".format( |
| 277 | + downloaded / (1024 * 1024), total / (1024 * 1024)) |
| 278 | + |
| 279 | + if error is not None: |
| 280 | + if attempt == max_retries: |
| 281 | + log("ERROR: Download failed: {}".format(error)) |
| 282 | + sys.exit(1) |
| 283 | + log("WARNING: Download incomplete (attempt {}): {}".format(attempt, error)) |
| 284 | + time.sleep(2) |
| 285 | + continue |
| 286 | + |
| 287 | + log("Saved: {}".format(os.path.basename(dest_path))) |
| 288 | + return |
233 | 289 |
|
234 | 290 |
|
235 | 291 | def _print_progress(downloaded, total): |
|
0 commit comments