mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-13 23:33:43 +02:00
116 lines
4.8 KiB
Python
116 lines
4.8 KiB
Python
"""Download helpers for the native Wine component installer.
|
|
|
|
Multi-source fetching with integrity checking, split out of
|
|
native_component_installer.py to keep that module within the file size limit.
|
|
"""
|
|
|
|
import hashlib
|
|
import logging
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Sequence, Union
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ComponentDownloadMixin:
|
|
"""Fetches component payloads. Expects the host class to provide `logger`,
|
|
`_emit_status()` and, where relevant, `_current_component`."""
|
|
|
|
def _download_file(self, url: Union[str, Sequence[str]], dest: Path,
|
|
sha256: Union[str, Sequence[str]] = "") -> bool:
|
|
urls = [url] if isinstance(url, str) else list(url)
|
|
|
|
if dest.is_file():
|
|
if not sha256:
|
|
return True
|
|
if self._verify_sha256(dest, sha256):
|
|
return True
|
|
self.logger.warning("SHA256 mismatch on cached %s, re-downloading", dest.name)
|
|
dest.unlink()
|
|
|
|
for index, candidate in enumerate(urls):
|
|
if not self._download_from_url(candidate, dest):
|
|
continue
|
|
if sha256 and not self._verify_sha256(dest, sha256):
|
|
self.logger.error("SHA256 mismatch on %s from %s", dest.name, candidate)
|
|
dest.unlink()
|
|
continue
|
|
return True
|
|
|
|
self.logger.error("All %d source(s) failed for %s", len(urls), dest.name)
|
|
return False
|
|
|
|
def _download_from_url(self, url: str, dest: Path) -> bool:
|
|
"""Download to a .part file and rename only once the transfer is verifiably
|
|
complete, so an interrupted download can never be left behind and trusted as a
|
|
cached copy on the next run."""
|
|
component = getattr(self, '_current_component', dest.stem)
|
|
self.logger.info("Downloading %s ...", dest.name)
|
|
self._emit_status(f"Downloading {dest.name}...")
|
|
partial = dest.with_name(dest.name + '.part')
|
|
try:
|
|
req = urllib.request.Request(url, headers={'User-Agent': 'Jackify/1.0'})
|
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
total = int(resp.headers.get('Content-Length', 0) or 0)
|
|
downloaded = 0
|
|
start = time.monotonic()
|
|
last_emit = start
|
|
chunk_size = 65536
|
|
with open(partial, 'wb') as f:
|
|
while True:
|
|
chunk = resp.read(chunk_size)
|
|
if not chunk:
|
|
break
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
now = time.monotonic()
|
|
if total > 0 and now - last_emit >= 0.5:
|
|
pct = downloaded / total * 100.0
|
|
elapsed = now - start
|
|
speed = downloaded / elapsed / 1048576.0 if elapsed > 0.05 else 0.0
|
|
self._emit_status(f"[NATIVE_DL] {component} {pct:.1f} {speed:.1f}")
|
|
last_emit = now
|
|
|
|
if downloaded == 0:
|
|
self.logger.error("Download from %s produced an empty file", url)
|
|
partial.unlink(missing_ok=True)
|
|
return False
|
|
if total > 0 and downloaded != total:
|
|
self.logger.error(
|
|
"Truncated download from %s: got %d bytes, expected %d",
|
|
url, downloaded, total,
|
|
)
|
|
partial.unlink(missing_ok=True)
|
|
return False
|
|
|
|
partial.replace(dest)
|
|
return True
|
|
except Exception as exc:
|
|
self.logger.error("Download failed for %s: %s", url, exc)
|
|
partial.unlink(missing_ok=True)
|
|
return False
|
|
|
|
def _verify_sha256(self, path: Path, expected: Union[str, Sequence[str]]) -> bool:
|
|
accepted = {expected.lower()} if isinstance(expected, str) else {e.lower() for e in expected}
|
|
h = hashlib.sha256()
|
|
try:
|
|
with open(path, 'rb') as f:
|
|
for chunk in iter(lambda: f.read(65536), b''):
|
|
h.update(chunk)
|
|
return h.hexdigest().lower() in accepted
|
|
except Exception:
|
|
return False
|
|
|
|
def _discard_cached_installer(self, path: Path) -> None:
|
|
"""Drop a cached installer that Wine refused to run. The redist URLs are not
|
|
version-pinned, so a corrupt cached copy is otherwise re-used on every retry and
|
|
no amount of re-running configuration would clear it."""
|
|
try:
|
|
path.unlink(missing_ok=True)
|
|
self.logger.info("Discarded cached installer %s so the next attempt re-downloads", path.name)
|
|
except Exception as exc:
|
|
self.logger.debug("Could not discard cached installer %s: %s", path.name, exc)
|
|
|