summaryrefslogtreecommitdiff
path: root/qutebrowser/browser/qtnetworkdownloads.py
diff options
context:
space:
mode:
Diffstat (limited to 'qutebrowser/browser/qtnetworkdownloads.py')
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py117
1 files changed, 53 insertions, 64 deletions
diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py
index a720402f5..242565a39 100644
--- a/qutebrowser/browser/qtnetworkdownloads.py
+++ b/qutebrowser/browser/qtnetworkdownloads.py
@@ -26,9 +26,9 @@ import functools
import dataclasses
from typing import Dict, IO, Optional
-from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl
-from PyQt5.QtWidgets import QApplication
-from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
+from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QTimer, QUrl
+from qutebrowser.qt.widgets import QApplication
+from qutebrowser.qt.network import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from qutebrowser.config import config, websettings
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg
@@ -62,12 +62,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
As soon as we know the file object, we copy self._buffer over and the next
readyRead will write to the real file object.
- Class attributes:
- _MAX_REDIRECTS: The maximum redirection count.
-
Attributes:
_retry_info: A _RetryInfo instance.
- _redirects: How many time we were redirected already.
_buffer: A BytesIO object to buffer incoming data until we know the
target file.
_read_timer: A Timer which reads the QNetworkReply into self._buffer
@@ -82,7 +78,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
arg 0: The new DownloadItem
"""
- _MAX_REDIRECTS = 10
adopt_download = pyqtSignal(object) # DownloadItem
def __init__(self, reply, manager):
@@ -102,7 +97,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._read_timer = usertypes.Timer(self, name='download-read-timer')
self._read_timer.setInterval(500)
self._read_timer.timeout.connect(self._on_read_timer_timeout)
- self._redirects = 0
self._url = reply.url()
self._init_reply(reply)
@@ -123,10 +117,12 @@ class DownloadItem(downloads.AbstractDownloadItem):
if self._reply is None:
log.downloads.debug("Reply gone while dying")
return
+
self._reply.downloadProgress.disconnect()
self._reply.finished.disconnect()
- self._reply.error.disconnect()
+ self._reply.errorOccurred.disconnect()
self._reply.readyRead.disconnect()
+
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
'problem, this method must only be called '
'once.'):
@@ -135,11 +131,22 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._reply.deleteLater()
self._reply = None
if self.fileobj is not None:
+ pos = self.fileobj.tell()
+ log.downloads.debug(f"File position at error: {pos}")
try:
self.fileobj.close()
except OSError:
log.downloads.exception("Error while closing file object")
+ if pos == 0:
+ # Emtpy remaining file
+ filename = self._get_open_filename()
+ log.downloads.debug(f"Removing empty file at {filename}")
+ try:
+ os.remove(filename)
+ except OSError:
+ log.downloads.exception("Error while removing empty file")
+
def _init_reply(self, reply):
"""Set a new reply and connect its signals.
@@ -150,11 +157,14 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.successful = False
self._reply = reply
reply.setReadBufferSize(16 * 1024 * 1024) # 16 MB
+
reply.downloadProgress.connect(self.stats.on_download_progress)
reply.finished.connect(self._on_reply_finished)
- reply.error.connect(self._on_reply_error)
+ reply.errorOccurred.connect(self._on_reply_error)
reply.readyRead.connect(self._on_ready_read)
reply.metaDataChanged.connect(self._on_meta_data_changed)
+ reply.redirected.connect(self._on_redirected)
+
self._retry_info = _RetryInfo(request=reply.request(),
manager=reply.manager())
if not self.fileobj:
@@ -162,9 +172,16 @@ class DownloadItem(downloads.AbstractDownloadItem):
# We could have got signals before we connected slots to them.
# Here no signals are connected to the DownloadItem yet, so we use a
# singleShot QTimer to emit them after they are connected.
- if reply.error() != QNetworkReply.NoError:
+ if reply.error() != QNetworkReply.NetworkError.NoError:
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
+ @pyqtSlot(QUrl)
+ def _on_redirected(self, url):
+ if self._reply is None:
+ log.downloads.warning(f"redirected: REPLY GONE -> {url}")
+ else:
+ log.downloads.debug(f"redirected: {self._reply.url()} -> {url}")
+
def _do_cancel(self):
self._read_timer.stop()
if self._reply is not None:
@@ -286,7 +303,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.fileobj.write(self._reply.readAll())
if self._autoclose:
self.fileobj.close()
- self.successful = self._reply.error() == QNetworkReply.NoError
+ self.successful = self._reply.error() == QNetworkReply.NetworkError.NoError
self._reply.close()
self._reply.deleteLater()
self._reply = None
@@ -307,9 +324,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
return
self._read_timer.stop()
self.stats.finish()
- is_redirected = self._handle_redirect()
- if is_redirected:
- return
log.downloads.debug("Reply finished, fileobj {}".format(self.fileobj))
if self.fileobj is not None:
# We can do a "delayed" write immediately to empty the buffer and
@@ -334,7 +348,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
@pyqtSlot('QNetworkReply::NetworkError')
def _on_reply_error(self, code):
"""Handle QNetworkReply errors."""
- if code == QNetworkReply.OperationCanceledError:
+ if code == QNetworkReply.NetworkError.OperationCanceledError:
return
if self._reply is None:
@@ -364,48 +378,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
for key, value in self._reply.rawHeaderPairs():
self.raw_headers[bytes(key)] = bytes(value)
- def _handle_redirect(self):
- """Handle an HTTP redirect.
-
- Return:
- True if the download was redirected, False otherwise.
- """
- assert self._reply is not None
- redirect = self._reply.attribute(
- QNetworkRequest.RedirectionTargetAttribute)
- if redirect is None or redirect.isEmpty():
- return False
- new_url = self._reply.url().resolved(redirect)
- new_request = self._reply.request()
- if new_url == new_request.url():
- return False
-
- if self._redirects > self._MAX_REDIRECTS:
- self._die("Maximum redirection count reached!")
- self.delete()
- return True # so on_reply_finished aborts
-
- log.downloads.debug("{}: Handling redirect".format(self))
- self._redirects += 1
- new_request.setUrl(new_url)
-
- old_reply = self._reply
- assert old_reply is not None
- old_reply.finished.disconnect(self._on_reply_finished)
-
- self._read_timer.stop()
- self._reply = None
- if self.fileobj is not None:
- self.fileobj.seek(0)
-
- log.downloads.debug("redirected: {} -> {}".format(
- old_reply.url(), new_request.url()))
- new_reply = old_reply.manager().get(new_request)
- self._init_reply(new_reply)
-
- old_reply.deleteLater()
- return True
-
def _uses_nam(self, nam):
"""Check if this download uses the given QNetworkAccessManager."""
assert self._retry_info is not None
@@ -422,8 +394,17 @@ class DownloadManager(downloads.AbstractDownloadManager):
Attributes:
_networkmanager: A NetworkManager for generic downloads.
+
+ Class attributes:
+ _MAX_REDIRECTS: The maximum redirection count.
"""
+ # Same as many browsers
+ # https://fetch.spec.whatwg.org/#http-redirect-fetch
+ # https://source.chromium.org/chromium/chromium/src/+/main:net/url_request/url_request.h;l=97;drc=3c19a2edb96d3d5b56a7481349a357fdbdf8ecf0
+ # https://stackoverflow.com/questions/9384474/in-chrome-how-many-redirects-are-too-many
+ _MAX_REDIRECTS = 20
+
def __init__(self, parent=None):
super().__init__(parent)
self._networkmanager = networkmanager.NetworkManager(
@@ -447,11 +428,19 @@ class DownloadManager(downloads.AbstractDownloadManager):
return None
req = QNetworkRequest(url)
- user_agent = websettings.user_agent(url)
- req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
+ user_agent = websettings.user_agent(url)
+ req.setHeader(QNetworkRequest.KnownHeaders.UserAgentHeader, user_agent)
if not cache:
- req.setAttribute(QNetworkRequest.CacheSaveControlAttribute, False)
+ req.setAttribute(QNetworkRequest.Attribute.CacheSaveControlAttribute, False)
+
+ # Needed for Qt 5, default on Qt 6
+ # We don't set this on the QNAM because QtWebKit handles redirects manually.
+ req.setAttribute(
+ QNetworkRequest.Attribute.RedirectPolicyAttribute,
+ QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy,
+ )
+ req.setMaximumRedirectsAllowed(self._MAX_REDIRECTS)
return self.get_request(req, **kwargs)
@@ -511,8 +500,8 @@ class DownloadManager(downloads.AbstractDownloadManager):
"""
# WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt.io/browse/QTBUG-42757
- request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
- QNetworkRequest.AlwaysNetwork)
+ request.setAttribute(QNetworkRequest.Attribute.CacheLoadControlAttribute,
+ QNetworkRequest.CacheLoadControl.AlwaysNetwork)
if suggested_fn is None:
suggested_fn = self._get_suggested_filename(request)