From fe041188f5d7d5f7eb3881ffbfcfa68d2dd6d2ab Mon Sep 17 00:00:00 2001 From: chrg6 Date: Thu, 12 Nov 2026 22:16:57 -0500 Subject: [PATCH] Give a resumed download's progress the whole file's size as its total. The total was "length + have or size or 0", which is the part file's size when a resumed answer names no length: the bar ran past its end. It is now the expected size, else the server's length plus the part, else unknown. Co-Authored-By: Claude Fable 5.2 --- launcher/net.py | 7 ++++++- tests/launcher_test.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/launcher/net.py b/launcher/net.py index 064f6a50..f58c0cca 100644 --- a/launcher/net.py +++ b/launcher/net.py @@ -262,7 +262,12 @@ def download(url, dest, sha256=None, size=None, progress=None, cancel=None, head status = getattr(r, 'status', 200) if have and status != 206: # the server ignores Range: start over have = 0 - total = int(r.headers.get('Content-Length') or 0) + have or size or 0 + # The whole file's size for progress: the expected one, else what the server says is still to come + # plus what the part file holds, else unknown (0: progress then gets the bytes so far as the total). + # "length + have or size or 0" took the part file's size for the whole when a resumed answer named + # no length, and the bar ran past its end. + coming = int(r.headers.get('Content-Length') or 0) + total = size or (coming + have if coming else 0) with open(part, 'ab' if have else 'wb') as f: done = have while True: diff --git a/tests/launcher_test.py b/tests/launcher_test.py index d6b74cdd..87e2e699 100644 --- a/tests/launcher_test.py +++ b/tests/launcher_test.py @@ -657,6 +657,42 @@ class NetTest(Tmp): with self.assertRaises(net.DownloadError): net.download(srv.url + 'missing.bin', self.tmp / 'out' / 'm.bin') + def test_the_download_total(self): + """progress gets the whole file's size as its total: the expected size, else the length the server names + plus what the part file holds, else the bytes so far. A resumed answer without a length gave the part + file's size as the total, so the bar passed 100 %.""" + data = os.urandom(2 * net.CHUNK + 500) + have = 1000 + + class Answer(io.BytesIO): # what net.request returns: the rest of the file after a Range request + status = 206 + length = None + + def __init__(self, headers): + super().__init__(data[have:]) + self.headers = headers + + def fetch(headers, **kw): + dest = self.tmp / 'total' / 'f.bin' + shutil.rmtree(dest.parent, ignore_errors=True) + dest.parent.mkdir() + dest.with_name('f.bin.part').write_bytes(data[:have]) + seen = [] + with mock.patch.object(net, 'request', lambda url, h, timeout, version: Answer(headers)): + net.download('https://example.invalid/f.bin', dest, progress=lambda d, t: seen.append((d, t)), **kw) + self.assertEqual(dest.read_bytes(), data) + self.assertEqual(seen[-1][0], len(data)) + return seen + + for d, t in fetch({}): # no length, no expected size + self.assertEqual(t, d) # (the part's 1000 bytes as the total before) + for d, t in fetch({'Content-Length': str(len(data) - have)}): # the length of the rest + self.assertEqual(t, len(data)) + for d, t in fetch({}, size=len(data)): # the expected size, whatever the server says + self.assertEqual(t, len(data)) + for d, t in fetch({'Content-Length': '7'}, size=len(data)): + self.assertEqual(t, len(data)) + def test_plain_http_redirects_and_oversize(self): with self.assertRaises(net.DownloadError): # refused before any connection net.request('http://example.invalid/f.bin')