Initial QtWebEngine download support
This commit is contained in:
parent
c876c3d244
commit
bf994cd8da
@ -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):
|
||||
|
||||
|
@ -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
|
||||
|
131
qutebrowser/browser/webengine/webenginedownloads.py
Normal file
131
qutebrowser/browser/webengine/webenginedownloads.py
Normal 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.
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user