Initial QtWebEngine download support

This commit is contained in:
Florian Bruhin 2016-11-01 20:43:12 +01:00
parent c876c3d244
commit bf994cd8da
4 changed files with 181 additions and 29 deletions

View File

@ -63,6 +63,11 @@ class UnsupportedAttribute:
pass
class UnsupportedOperationError(Exception):
"""Raised when an operation is not supported with the given backend."""
def download_dir():
"""Get the download directory to use."""
directory = config.get('storage', 'download-directory')
@ -396,7 +401,6 @@ class AbstractDownloadItem(QObject):
"""
self._do_cancel()
log.downloads.debug("cancelled")
self.cancelled.emit()
if remove_data:
self.delete()
self.done = True
@ -471,7 +475,15 @@ class AbstractDownloadItem(QObject):
"""Finish initialization based on self._filename."""
raise NotImplementedError
def set_filename(self, filename):
def _set_fileobj(self, fileobj):
"""Set a file object to save the download to.
Note that some backends (QtWebEngine) will simply access the .name
attribute and not actually use the file object directly.
"""
raise NotImplementedError
def _set_filename(self, filename):
"""Set the filename to save the download to.
Args:
@ -542,7 +554,23 @@ class AbstractDownloadItem(QObject):
Args:
target: The usertypes.DownloadTarget for this download.
"""
raise NotImplementedError
if isinstance(target, usertypes.FileObjDownloadTarget):
raise UnsupportedAttribute("FileObjDownloadTarget is unsupported")
elif isinstance(target, usertypes.FileDownloadTarget):
self._set_filename(target.filename)
elif isinstance(target, usertypes.OpenFileDownloadTarget):
try:
fobj = temp_download_manager.get_tmpfile(self.basename)
except OSError as exc:
msg = "Download error: {}".format(exc)
message.error(msg)
self.cancel()
return
self.finished.connect(
functools.partial(self._open_if_successful, target.cmdline))
self._set_fileobj(fobj)
else: # pragma: no cover
raise ValueError("Unsupported download target: {}".format(target))
class AbstractDownloadManager(QObject):
@ -659,6 +687,14 @@ class AbstractDownloadManager(QObject):
if first_idx is not None:
self.data_changed.emit(first_idx, -1)
def _init_filename_question(self, question, download):
"""Set up an existing filename question with a download."""
question.mode = usertypes.PromptMode.download
question.answered.connect(download.set_target)
question.cancelled.connect(download.cancel)
download.cancelled.connect(question.abort)
download.error.connect(question.abort)
class DownloadModel(QAbstractListModel):

View File

@ -87,9 +87,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.fileobj = None
self.raw_headers = {}
self._filename = None
self._dead = False
self._manager = manager
self._retry_info = None
self._reply = None
@ -171,6 +168,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._reply = None
if self.fileobj is not None:
self.fileobj.close()
self.cancelled.emit()
@pyqtSlot()
def retry(self):
@ -354,23 +352,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
if isinstance(target, usertypes.FileObjDownloadTarget):
self._set_fileobj(target.fileobj)
self.autoclose = False
elif isinstance(target, usertypes.FileDownloadTarget):
self.set_filename(target.filename)
elif isinstance(target, usertypes.OpenFileDownloadTarget):
try:
fobj = downloads.temp_download_manager.get_tmpfile(
self.basename)
except OSError as exc:
msg = "Download error: {}".format(exc)
message.error(msg)
self.cancel()
return
self.finished.connect(
functools.partial(self._open_if_successful, target.cmdline))
self.autoclose = True
self._set_fileobj(fobj)
else: # pragma: no cover
raise ValueError("Unsupported download target: {}".format(target))
else:
super().set_target(target)
class DownloadManager(downloads.AbstractDownloadManager):
@ -506,11 +489,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
question = downloads.get_filename_question(
suggested_filename=suggested_filename, url=reply.url(),
parent=self)
question.mode = usertypes.PromptMode.download
question.answered.connect(download.set_target)
question.cancelled.connect(download.cancel)
download.cancelled.connect(question.abort)
download.error.connect(question.abort)
self._init_filename_question(question, download)
message.global_bridge.ask(question, blocking=False)
return download

View File

@ -0,0 +1,131 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""QtWebEngine specific code for downloads."""
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
from PyQt5.QtWidgets import QApplication
from qutebrowser.browser import downloads
from qutebrowser.utils import debug, usertypes, message, log
class DownloadItem(downloads.AbstractDownloadItem):
"""A wrapper over a QWebEngineDownloadItem.
Attributes:
_qt_item: The wrapped item.
"""
def __init__(self, qt_item, parent=None):
super().__init__(parent)
self._qt_item = qt_item
qt_item.downloadProgress.connect(self.stats.on_download_progress)
qt_item.stateChanged.connect(self._on_state_changed)
@pyqtSlot(QWebEngineDownloadItem.DownloadState)
def _on_state_changed(self, state):
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
log.downloads.debug("State for {!r} changed to {}".format(
self, state_name))
if state == QWebEngineDownloadItem.DownloadRequested:
pass
elif state == QWebEngineDownloadItem.DownloadInProgress:
pass
elif state == QWebEngineDownloadItem.DownloadCompleted:
self.successful = True
self.done = True
self.finished.emit()
elif state == QWebEngineDownloadItem.DownloadCancelled:
self.successful = False
self.done = True
self.cancelled.emit()
elif state == QWebEngineDownloadItem.DownloadInterrupted:
self.successful = False
self.done = True
# https://bugreports.qt.io/browse/QTBUG-56839
self.error.emit("Download failed")
else:
raise ValueError("_on_state_changed was called with unknown state "
"{}".format(state_name))
def _do_die(self):
self._qt_item.downloadProgress.disconnect()
self._qt_item.cancel()
def _do_cancel(self):
self._qt_item.cancel()
def retry(self):
# https://bugreports.qt.io/browse/QTBUG-56840
raise downloads.UnsupportedOperationError
def get_open_filename(self):
return self._filename
def _set_fileobj(self, fileobj):
self._set_filename(fileobj.name)
def _ensure_can_set_filename(self, filename):
state = self._qt_item.state()
if state != QWebEngineDownloadItem.DownloadRequested:
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
raise ValueError("Trying to set filename {} on {!r} which is state "
"{} (not in requested state)!".format(
filename, self, state_name))
def _after_set_filename(self):
self._qt_item.setPath(self._filename)
self._qt_item.accept()
class DownloadManager(downloads.AbstractDownloadManager):
"""Manager for currently running downloads."""
def install(self, profile):
"""Set up the download manager on a QWebEngineProfile."""
profile.downloadRequested.connect(self.handle_download,
Qt.DirectConnection)
@pyqtSlot(QWebEngineDownloadItem)
def handle_download(self, qt_item):
download = DownloadItem(qt_item)
self._init_item(download, auto_remove=False,
suggested_filename=qt_item.path())
filename = downloads.immediate_download_path()
if filename is not None:
# User doesn't want to be asked, so just use the download_dir
target = usertypes.FileDownloadTarget(filename)
download.set_target(target)
return
# Ask the user for a filename - needs to be blocking!
question = downloads.get_filename_question(
suggested_filename=qt_item.path(), url=qt_item.url(),
parent=self)
self._init_filename_question(question, download)
message.global_bridge.ask(question, blocking=True)
# The filename is set via the question.answered signal, connected in
# _init_filename_question.

View File

@ -34,7 +34,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
from qutebrowser.browser import browsertab, mouse
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme)
interceptor, webenginequtescheme,
webenginedownloads)
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg)
@ -61,6 +62,11 @@ def init():
host_blocker, parent=app)
req_interceptor.install(profile)
log.init.debug("Initializing QtWebEngine downloads...")
download_manager = webenginedownloads.DownloadManager(parent=app)
download_manager.install(profile)
objreg.register('webengine-download-manager', download_manager)
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
_JS_WORLD_MAP = {