jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1326394?usp=email )

Change subject: download_dump: Harden streamed downloads
......................................................................

download_dump: Harden streamed downloads

Streamed responses were never closed, including when the server
returned an error status. Missing Content-Length headers also raised
KeyError before the intended warning and progress-free fallback could
run.

Manage each response with its context manager and treat a missing
length as unknown so the complete stream is still written.

Change-Id: Ie229f207cb0a9e59268dca4764b98e481e3b988c
---
M scripts/download_dump.py
M tests/__init__.py
A tests/download_dump_tests.py
3 files changed, 99 insertions(+), 42 deletions(-)

Approvals:
  Xqt: Looks good to me, approved
  jenkins-bot: Verified




diff --git a/scripts/download_dump.py b/scripts/download_dump.py
index 800a9c0..8721a24 100755
--- a/scripts/download_dump.py
+++ b/scripts/download_dump.py
@@ -111,53 +111,56 @@
                     url = (f'https://dumps.wikimedia.org/{self.opt.wikiname}/'
                            f'{self.opt.dumpdate}/{download_filename}')
                     pywikibot.info('Downloading file from ' + url)
-                    response = fetch(url, stream=True)
+                    with fetch(url, stream=True) as response:
+                        if response.status_code != HTTPStatus.OK:
+                            if response.status_code == HTTPStatus.NOT_FOUND:
+                                pywikibot.info(
+                                    'File with name {filename!r}, from '
+                                    'dumpdate {dumpdate!r}, and wiki '
+                                    '{wikiname!r} ({url}) '
+                                    "isn't available in the Wikimedia Dumps"
+                                    .format(url=url, **self.opt))
+                            else:
+                                pywikibot.info(HTTPStatus(
+                                    response.status_code).description)
+                            return

-                    if response.status_code != HTTPStatus.OK:
-                        if response.status_code == HTTPStatus.NOT_FOUND:
-                            pywikibot.info(
-                                'File with name {filename!r}, from dumpdate '
-                                '{dumpdate!r}, and wiki {wikiname!r} ({url}) '
-                                "isn't available in the Wikimedia Dumps"
-                                .format(url=url, **self.opt))
-                        else:
-                            pywikibot.info(
-                                HTTPStatus(response.status_code).description)
-                        return
+                        with open(file_current_storepath,
+                                  'wb') as result_file:
+                            total = int(response.headers.get(
+                                'content-length', -1))
+                            if total == -1:
+                                pywikibot.warning(
+                                    "'content-length' missing in response "
+                                    'headers')
+                            downloaded = 0
+                            parts = 50
+                            display_string = ''
 
-                    with open(file_current_storepath, 'wb') as result_file:
-                        total = int(response.headers['content-length'])
-                        if total == -1:
-                            pywikibot.warning("'content-length' missing in "
-                                              'response headers')
-                        downloaded = 0
-                        parts = 50
-                        display_string = ''
+                            pywikibot.info()
+                            for data in response.iter_content(100 * 1024):
+                                result_file.write(data)

-                        pywikibot.info()
-                        for data in response.iter_content(100 * 1024):
-                            result_file.write(data)
+                                if total <= 0:
+                                    continue

-                            if total <= 0:
-                                continue
+                                downloaded += len(data)
+                                done = int(parts * downloaded / total)
+                                display = map(convert_from_bytes,
+                                              (downloaded, total))
+                                prior_display = display_string
+                                display_string = '\r|{}{}|{}{}/{}'.format(
+                                    '=' * done,
+                                    '-' * (parts - done),
+                                    ' ' * 5,
+                                    *display)
+                                # Add whitespace to cover up prior bar
+                                display_string += ' ' * (
+                                    len(prior_display.rstrip())
+                                    - len(display_string.rstrip()))

-                            downloaded += len(data)
-                            done = int(parts * downloaded / total)
-                            display = map(convert_from_bytes,
-                                          (downloaded, total))
-                            prior_display = display_string
-                            display_string = '\r|{}{}|{}{}/{}'.format(
-                                '=' * done,
-                                '-' * (parts - done),
-                                ' ' * 5,
-                                *display)
-                            # Add whitespace to cover up prior bar
-                            display_string += ' ' * (
-                                len(prior_display.rstrip())
-                                - len(display_string.rstrip()))
-
-                            pywikibot.info(display_string, newline=False)
-                        pywikibot.info()
+                                pywikibot.info(display_string, newline=False)
+                            pywikibot.info()

                 # Rename the temporary file to the target file
                 # if the download completes successfully
diff --git a/tests/__init__.py b/tests/__init__.py
index b3fe091..7c0145f 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -183,6 +183,7 @@
     'commonscat',
     'data_ingestion',
     'deletionbot',
+    'download_dump',
     'fixing_redirects',
     'generate_family_file',
     'generate_user_files',
diff --git a/tests/download_dump_tests.py b/tests/download_dump_tests.py
new file mode 100755
index 0000000..7d090be
--- /dev/null
+++ b/tests/download_dump_tests.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+#
+# (C) Pywikibot team, 2026
+#
+# Distributed under the terms of the MIT license.
+#
+"""Tests for the download_dump script."""
+from __future__ import annotations
+
+import unittest
+from http import HTTPStatus
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest.mock import MagicMock, patch
+
+from scripts import download_dump
+from tests.aspects import TestCase
+
+
+class DownloadDumpBotTestCase(TestCase):
+
+    """Test :class:`download_dump.DownloadDumpBot`."""
+
+    net = False
+
+    @patch.object(download_dump.DownloadDumpBot, 'get_dump_name',
+                  return_value=None)
+    @patch.object(download_dump, 'fetch')
+    def test_download_without_content_length(
+        self, fetch_mock, get_dump_name_mock,
+    ) -> None:
+        """Test downloading a response without a content length."""
+        response = MagicMock()
+        response.__enter__.return_value = response
+        response.status_code = HTTPStatus.OK
+        response.headers = {}
+        response.iter_content.return_value = [b'first', b'second']
+        fetch_mock.return_value = response
+
+        with TemporaryDirectory() as directory:
+            bot = download_dump.DownloadDumpBot(
+                wikiname='enwiki', filename='pages.xml.bz2',
+                storepath=directory, dumpdate='latest')
+            bot.run()
+
+            path = Path(directory) / 'enwiki-latest-pages.xml.bz2'
+            self.assertEqual(path.read_bytes(), b'firstsecond')
+
+        response.__exit__.assert_called_once()
+
+
+if __name__ == '__main__':
+    unittest.main()

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1326394?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings?usp=email

Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Ie229f207cb0a9e59268dca4764b98e481e3b988c
Gerrit-Change-Number: 1326394
Gerrit-PatchSet: 2
Gerrit-Owner: Mahveotm <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to