File size: 2,126 Bytes
d4c2896 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | #!/usr/bin/env python3
"""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: # noqa: BLE001
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 # already complete
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: # range from EOF → the file is already complete
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: # noqa: BLE001
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)
|