mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-28 17:02:17 +01:00
Retry transient iOS bundle package downloads
This commit is contained in:
1 parent
13ed576abd
commit
9ae78aef60
2 files changed
+76
-4
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user