| |
| """Resumable download (Range) with retries. Survives under setsid (python does; |
| curl/wget under setsid died with the bash tool). Usage: dl.py URL DEST [RETRIES] |
| |
| Fix 2026-08-05: if the server answers 416 (Range past EOF), the file is already |
| complete → exit OK after comparing against the real Content-Length (HEAD). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import sys |
| import time |
| import urllib.error |
| import urllib.request |
|
|
| URL, DEST = sys.argv[1], sys.argv[2] |
| RETRIES = int(sys.argv[3]) if len(sys.argv) > 3 else 30 |
|
|
|
|
| def content_length() -> int | None: |
| """Total length of the remote file (HEAD, following redirects).""" |
| try: |
| req = urllib.request.Request(URL, method="HEAD") |
| with urllib.request.urlopen(req, timeout=30) as r: |
| return int(r.headers.get("Content-Length", 0)) or None |
| except Exception: |
| return None |
|
|
|
|
| def download() -> bool: |
| pos = os.path.getsize(DEST) if os.path.exists(DEST) else 0 |
| total = content_length() |
| if total is not None and pos >= total: |
| return True |
| req = urllib.request.Request(URL, headers={"Range": f"bytes={pos}-"}) |
| with urllib.request.urlopen(req) as r, open(DEST, "ab") as f: |
| while True: |
| chunk = r.read(1 << 20) |
| if not chunk: |
| return True |
| f.write(chunk) |
|
|
|
|
| for attempt in range(1, RETRIES + 1): |
| try: |
| if download(): |
| print(f"OK {os.path.getsize(DEST)}", flush=True) |
| sys.exit(0) |
| except urllib.error.HTTPError as e: |
| if e.code == 416: |
| print(f"OK 416-complete {os.path.getsize(DEST)}", flush=True) |
| sys.exit(0) |
| print( |
| f"attempt {attempt} failed: HTTP {e.code} (partial {os.path.getsize(DEST)})", |
| flush=True, |
| ) |
| except Exception as e: |
| print(f"attempt {attempt} failed: {e!r} (partial {os.path.getsize(DEST)})", flush=True) |
| time.sleep(5) |
| print(f"EXHAUSTED after {RETRIES} attempts", flush=True) |
| sys.exit(1) |
|
|