diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 39f1a85e7..845a083c9 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -53,6 +53,7 @@ from qutebrowser.commands.managers import CommandManager, SearchManager from qutebrowser.config.iniparsers import ReadWriteConfigParser from qutebrowser.config.lineparser import LineConfigParser from qutebrowser.browser.cookies import CookieJar +from qutebrowser.browser.downloads import DownloadManager from qutebrowser.utils.message import MessageBridge from qutebrowser.utils.misc import (get_standard_dir, actute_warning, get_qt_args) @@ -132,6 +133,7 @@ class Application(QApplication): self.networkmanager = NetworkManager(self.cookiejar) self.commandmanager = CommandManager() self.searchmanager = SearchManager() + self.downloadmanager = DownloadManager() self.mainwindow = MainWindow() self.modeman.mainwindow = self.mainwindow @@ -386,6 +388,9 @@ class Application(QApplication): cmd.update_completion.connect(completer.on_update_completion) completer.change_completed_part.connect(cmd.on_change_completed_part) + # downloads + tabs.start_download.connect(self.downloadmanager.fetch) + def _recover_pages(self): """Try to recover all open pages. diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py new file mode 100644 index 000000000..4877ce526 --- /dev/null +++ b/qutebrowser/browser/downloads.py @@ -0,0 +1,88 @@ +# Copyright 2014 Florian Bruhin (The Compiler) +# +# 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 . + +"""Download manager.""" + +import os.path + +from PyQt5.QtCore import pyqtSlot, QObject + +from qutebrowser.utils.log import downloads as logger + + +class DownloadManager(QObject): + + """Manager for running downloads.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.downloads = [] + + def _get_filename(self, reply): + """Get a suitable filename to download a file to. + + Args: + reply: The QNetworkReply to get a filename for.""" + filename = None + # First check if the Content-Disposition header has a filename + # attribute. + if reply.hasRawHeader('Content-Disposition'): + header = reply.rawHeader('Content-Disposition') + data = header.split(':', maxsplit=1)[1].strip() + for pair in data.split(';'): + if '=' in pair: + key, value = pair.split('=') + if key == 'filename': + filename = value.strip('"') + break + # Then try to get filename from url + if not filename: + filename = reply.url().path() + # If that fails as well, use a fallback + if not filename: + filename = 'qutebrowser-download' + return os.path.basename(filename) + + @pyqtSlot('QNetworkReply') + def fetch(self, reply): + """Download a QNetworkReply to disk. + + Args: + reply: The QNetworkReply to download. + """ + filename = self._get_filename(reply) + logger.debug("fetch: {} -> {}".format(reply.url(), filename)) + reply.downloadProgress.connect(self.on_download_progress) + reply.readyRead.connect(self.on_ready_read) + reply.finished.connect(self.on_finished) + + @pyqtSlot(int, int) + def on_download_progress(self, done, total): + if total == -1: + perc = '???' + else: + perc = 100 * done / total + logger.debug("{}% done".format(perc)) + + @pyqtSlot() + def on_ready_read(self): + logger.debug("readyread") + self.sender().readAll() + + @pyqtSlot() + def on_finished(self): + logger.debug("finished") diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 0ff2ebb01..8b395e836 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -18,7 +18,7 @@ """The main browser widgets.""" import sip -from PyQt5.QtCore import QCoreApplication +from PyQt5.QtCore import QCoreApplication, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtWidgets import QFileDialog from PyQt5.QtPrintSupport import QPrintDialog @@ -39,8 +39,13 @@ class BrowserPage(QWebPage): Attributes: _extension_handlers: Mapping of QWebPage extensions to their handlers. network_access_manager: The QNetworkAccessManager used. + + Signals: + start_download: Emitted when a file should be downloaded. """ + start_download = pyqtSignal('QNetworkReply*') + def __init__(self, parent=None): super().__init__(parent) self._extension_handlers = { @@ -50,6 +55,7 @@ class BrowserPage(QWebPage): self.setNetworkAccessManager( QCoreApplication.instance().networkmanager) self.printRequested.connect(self.on_print_requested) + self.downloadRequested.connect(self.on_download_requested) def _handle_errorpage(self, opt, out): """Display an error page if needed. @@ -118,6 +124,17 @@ class BrowserPage(QWebPage): printdiag = QPrintDialog() printdiag.open(lambda: frame.print(printdiag.printer())) + @pyqtSlot('QNetworkRequest') + def on_download_requested(self, request): + """Called when the user wants to download a link. + + Emit: + start_download: Emitted with the QNetworkReply associated with the + passed request. + """ + reply = self.networkAccessManager().get(request) + self.start_download.emit(reply) + def userAgentForUrl(self, url): """Override QWebPage::userAgentForUrl to customize the user agent.""" ua = config.get('network', 'user-agent') diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 2466be5ca..b6ae5d007 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -66,6 +66,7 @@ init = getLogger('init') signals = getLogger('signals') hints = getLogger('hints') keyboard = getLogger('keyboard') +downloads = getLogger('downloads') js = getLogger('js') qt = getLogger('qt') diff --git a/qutebrowser/widgets/tabbedbrowser.py b/qutebrowser/widgets/tabbedbrowser.py index 7bec5ebd4..8fc88ff42 100644 --- a/qutebrowser/widgets/tabbedbrowser.py +++ b/qutebrowser/widgets/tabbedbrowser.py @@ -72,6 +72,8 @@ class TabbedBrowser(TabWidget): resized: Emitted when the browser window has resized, so the completion widget can adjust its size to it. arg: The new size. + start_download: Emitted when any tab wants to start downloading + something. """ cur_progress = pyqtSignal(int) @@ -82,6 +84,7 @@ class TabbedBrowser(TabWidget): cur_link_hovered = pyqtSignal(str, str, str) cur_scroll_perc_changed = pyqtSignal(int, int) cur_load_status_changed = pyqtSignal(str) + start_download = pyqtSignal('QNetworkReply*') hint_strings_updated = pyqtSignal(list) shutdown_complete = pyqtSignal() quit = pyqtSignal() @@ -150,6 +153,9 @@ class TabbedBrowser(TabWidget): # hintmanager tab.hintmanager.hint_strings_updated.connect(self.hint_strings_updated) tab.hintmanager.openurl.connect(self.cmd.openurl) + # downloads + tab.page().unsupportedContent.connect(self.start_download) + tab.page().start_download.connect(self.start_download) # misc tab.titleChanged.connect(self.on_title_changed) tab.iconChanged.connect(self.on_icon_changed)