Retry transient iOS bundle package downloads

This commit is contained in:
SAPHID committed 2026-10-28 13:51:47 +00:00
1 parent 13ed576abd
commit 9ae78aef60
2 files changed
+76 -4

No files matched your search

+23 -4
View File
@@ -7,8 +7,11 @@ ios/scripts/make_frame_bundle.py runs this; app/build/fetch-deps.js does the sam
in Node. Usage: python3 frame/kdeconnect/fetch.py
"""
import hashlib
import http.client
import json
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
@@ -24,6 +27,25 @@ def sha256(data):
return hashlib.sha256(data).hexdigest()
def download_verified(url, expected_sha256):
for attempt in range(3):
try:
with urllib.request.urlopen(url, timeout=120) as response:
data = response.read()
except (OSError, http.client.HTTPException) as error:
# GitHub release downloads occasionally close during connection or
# transfer. Retry those failures, but fail fast on permanent HTTP errors.
if isinstance(error, urllib.error.HTTPError) and error.code not in (408, 429) and error.code < 500:
raise
if attempt == 2:
raise
time.sleep(attempt + 1)
continue
if sha256(data) != expected_sha256:
raise SystemExit(f"checksum mismatch for {url}: {sha256(data)}")
return data
def fetch():
m = manifest()
PACKAGES.mkdir(exist_ok=True)
@@ -32,10 +54,7 @@ def fetch():
if path.is_file() and sha256(path.read_bytes()) == p["sha256"]:
continue
url = m["release"] + p["file"]
with urllib.request.urlopen(url, timeout=120) as r:
data = r.read()
if sha256(data) != p["sha256"]:
raise SystemExit(f"checksum mismatch for {url}: {sha256(data)}")
data = download_verified(url, p["sha256"])
tmp = path.with_suffix(".part")
tmp.write_bytes(data)
tmp.replace(path)
+53
View File
@@ -0,0 +1,53 @@
"""The iOS Frame bundle must recover transient pinned package downloads."""
import hashlib
import http.client
import importlib.util
from pathlib import Path
import unittest
from unittest.mock import patch
import urllib.error
import io
ROOT = Path(__file__).resolve().parent.parent
SPEC = importlib.util.spec_from_file_location('kdeconnect_fetch', ROOT / 'frame/kdeconnect/fetch.py')
fetch = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(fetch)
class VerifiedDownload(unittest.TestCase):
def test_remote_disconnect_retries_then_checks_hash(self):
data = b'pinned package'
digest = hashlib.sha256(data).hexdigest()
with patch.object(fetch.urllib.request, 'urlopen', side_effect=[
http.client.RemoteDisconnected('closed'), io.BytesIO(data)]) as open_url, \
patch.object(fetch.time, 'sleep') as sleep:
self.assertEqual(fetch.download_verified('https://example.invalid/package', digest), data)
self.assertEqual(open_url.call_count, 2)
sleep.assert_called_once_with(1)
def test_permanent_http_error_and_wrong_hash_fail_closed(self):
url = 'https://example.invalid/package'
missing = urllib.error.HTTPError(url, 404, 'missing', {}, None)
with patch.object(fetch.urllib.request, 'urlopen', side_effect=missing) as open_url, \
patch.object(fetch.time, 'sleep') as sleep:
with self.assertRaises(urllib.error.HTTPError):
fetch.download_verified(url, '0' * 64)
open_url.assert_called_once()
sleep.assert_not_called()
with patch.object(fetch.urllib.request, 'urlopen', return_value=io.BytesIO(b'changed')) as open_url:
with self.assertRaisesRegex(SystemExit, 'checksum mismatch'):
fetch.download_verified(url, '0' * 64)
open_url.assert_called_once()
def test_exhausted_transient_error_is_not_silenced(self):
with patch.object(fetch.urllib.request, 'urlopen', side_effect=http.client.RemoteDisconnected('closed')) as open_url, \
patch.object(fetch.time, 'sleep') as sleep:
with self.assertRaises(http.client.RemoteDisconnected):
fetch.download_verified('https://example.invalid/package', '0' * 64)
self.assertEqual(open_url.call_count, 3)
self.assertEqual(sleep.call_count, 2)
if __name__ == '__main__':
unittest.main()