diff options
Diffstat (limited to 'qutebrowser/browser/qtnetworkdownloads.py')
-rw-r--r-- | qutebrowser/browser/qtnetworkdownloads.py | 117 |
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) |