"""Download a release archive and extract it. A complete local file is extracted without downloading again.""" import tarfile import urllib.error import urllib.request import zipfile from pathlib import Path UA = "LLLL-tools" def _archive_name(path: Path) -> str: name = path.name.lower() if name.endswith(".part"): name = name[: -len(".part")] return name def archive_complete(path: Path) -> bool: if not path.is_file() or path.stat().st_size == 0: return False name = _archive_name(path) try: if name.endswith(".zip") or name.endswith(".ipa"): with zipfile.ZipFile(path) as archive: return bool(archive.infolist()) if ".tar." in name or name.endswith(".tgz"): mode = "r:xz" if name.endswith(".tar.xz") else "r:gz" with tarfile.open(path, mode) as archive: for _ in archive: pass return True except (OSError, tarfile.TarError, zipfile.BadZipFile): return False return False def _total_size(resp, have: int, code: int) -> int | None: if code == 206: content_range = resp.headers.get("Content-Range") if content_range and "/" in content_range: total = content_range.rsplit("/", 1)[-1] if total.isdigit(): return int(total) length = resp.headers.get("Content-Length") if length and length.isdigit(): return have + int(length) return None length = resp.headers.get("Content-Length") if length and length.isdigit(): return int(length) return None def ensure_archive(url: str, dest: Path) -> None: dest.parent.mkdir(parents=True, exist_ok=True) part = dest.with_name(dest.name + ".part") if archive_complete(dest): return if archive_complete(part): part.replace(dest) return if dest.exists() and not part.exists(): dest.replace(part) elif dest.exists(): dest.unlink() have = part.stat().st_size if part.exists() else 0 headers = {"User-Agent": UA} if have: headers["Range"] = f"bytes={have}-" request = urllib.request.Request(url, headers=headers) try: response = urllib.request.urlopen(request, timeout=120) except urllib.error.HTTPError as exc: if exc.code == 416 and archive_complete(part): part.replace(dest) return print(f"下载失败:{exc.code}") raise SystemExit(2) except (urllib.error.URLError, TimeoutError, OSError): print("下载未完成") raise SystemExit(2) try: with response: code = getattr(response, "status", 200) if code == 200: have = 0 elif code != 206: print(f"下载失败:{code}") raise SystemExit(2) total = _total_size(response, have, code) shown = -1 with part.open("wb" if code == 200 else "ab") as output: done = have if code == 206 else 0 while True: chunk = response.read(1024 * 1024) if not chunk: break output.write(chunk) done += len(chunk) if not total: continue pct = min(100, done * 100 // total) if pct // 10 != shown: shown = pct // 10 print(f"\r{pct}%", end="", flush=True) if shown >= 0: print() except (urllib.error.URLError, TimeoutError, OSError): print("下载未完成") raise SystemExit(2) if archive_complete(part): part.replace(dest) return if total and part.exists() and part.stat().st_size == total: part.unlink(missing_ok=True) print("下载内容无法解压") raise SystemExit(2) print("下载未完成") raise SystemExit(2) def extract_archive(archive: Path, dest: Path) -> None: dest.mkdir(parents=True, exist_ok=True) root = dest.resolve() name = archive.name.lower() if name.endswith(".zip"): with zipfile.ZipFile(archive) as zipped: for info in zipped.infolist(): target = (dest / info.filename).resolve() if not target.is_relative_to(root): raise SystemExit("压缩包路径不安全") zipped.extractall(dest) return with tarfile.open(archive) as packed: packed.extractall(dest, filter="data")