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 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-12 22:16:57 -05:00
parent c5aee51601
commit fe041188f5
2 changed files with 42 additions and 1 deletions

View File

@@ -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:

View File

@@ -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')