Handle download errors and handle everything async

This commit is contained in:
Florian Bruhin 2014-06-12 17:50:09 +02:00
parent c91dced99f
commit ad7856569f

View File

@ -20,6 +20,7 @@
import os.path import os.path
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QTimer from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QTimer
from PyQt5.QtNetwork import QNetworkReply
import qutebrowser.config.config as config import qutebrowser.config.config as config
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
@ -51,39 +52,53 @@ class DownloadItem(QObject):
percentage_changed: The download percentage changed. percentage_changed: The download percentage changed.
arg: The new percentage, -1 if unknown. arg: The new percentage, -1 if unknown.
finished: The download was finished. finished: The download was finished.
error: An error with the download occured.
arg: The error message as string.
""" """
REFRESH_INTERVAL = 200 REFRESH_INTERVAL = 200
speed_changed = pyqtSignal(float) speed_changed = pyqtSignal(float)
percentage_changed = pyqtSignal(int) percentage_changed = pyqtSignal(int)
finished = pyqtSignal() finished = pyqtSignal()
error = pyqtSignal(str)
def __init__(self, reply, filename, parent=None): def __init__(self, reply, parent=None):
"""Constructor. """Constructor.
Args: Args:
reply: The QNetworkReply to download. reply: The QNetworkReply to download.
filename: The full filename to save the download to.
""" """
super().__init__(parent) super().__init__(parent)
self.reply = reply self.reply = reply
self.bytes_done = None self.bytes_done = None
self.bytes_total = None self.bytes_total = None
self.speed = None self.speed = None
self.fileobj = None
self._do_delayed_write = False
self._last_done = None self._last_done = None
self._last_percentage = None self._last_percentage = None
# FIXME exceptions reply.setReadBufferSize(16 * 1024 * 1024)
self.fileobj = open(filename, 'wb')
reply.downloadProgress.connect(self.on_download_progress) reply.downloadProgress.connect(self.on_download_progress)
reply.finished.connect(self.on_finished) reply.finished.connect(self.on_reply_finished)
reply.finished.connect(self.finished) reply.error.connect(self.on_reply_error)
reply.error.connect(self.on_error)
reply.readyRead.connect(self.on_ready_read) reply.readyRead.connect(self.on_ready_read)
self.timer = QTimer() self.timer = QTimer()
self.timer.timeout.connect(self.update_speed) self.timer.timeout.connect(self.update_speed)
self.timer.setInterval(self.REFRESH_INTERVAL) self.timer.setInterval(self.REFRESH_INTERVAL)
self.timer.start() self.timer.start()
def _die(self, msg):
"""Abort the download and emit an error."""
self.error.emit(msg)
self.reply.abort()
self.reply.deleteLater()
if self.fileobj is not None:
try:
self.fileobj.close()
except OSError as e:
self.error.emit(e.strerror)
self.finished.emit()
@property @property
def percentage(self): def percentage(self):
"""Property to get the current download percentage.""" """Property to get the current download percentage."""
@ -96,6 +111,48 @@ class DownloadItem(QObject):
else: else:
return 100 * self.bytes_done / self.bytes_total return 100 * self.bytes_done / self.bytes_total
def cancel(self):
"""Cancel the download."""
logger.debug("cancelled")
self.reply.abort()
self.reply.deleteLater()
self.finished.emit()
def set_filename(self, filename):
"""Set the filename to save the download to.
Args:
filename: The full filename to save the download to.
None: special value to stop the download.
"""
if self.fileobj is not None:
raise ValueError("Filename was already set! filename: {}, "
"existing: {}".format(filename, self.fileobj))
try:
self.fileobj = open(filename, 'wb')
if self._do_delayed_write:
# Downloading to the buffer in RAM has already finished so we
# write out the data and clean up now.
self.delayed_write()
else:
# Since the buffer already might be full, on_ready_read might
# not be called at all anymore, so we force it here to flush
# the buffer and continue receiving new data.
self.on_ready_read()
except OSError as e:
self._die(e.strerror)
def delayed_write(self):
"""Write buffered data to disk and finish the QNetworkReply."""
logger.debug("Doing delayed write...")
self._do_delayed_write = False
self.fileobj.write(self.reply.readAll())
self.fileobj.close()
self.reply.close()
self.reply.deleteLater()
self.finished.emit()
logger.debug("Download finished")
@pyqtSlot(int, int) @pyqtSlot(int, int)
def on_download_progress(self, bytes_done, bytes_total): def on_download_progress(self, bytes_done, bytes_total):
"""Upload local variables when the download progress changed. """Upload local variables when the download progress changed.
@ -113,21 +170,43 @@ class DownloadItem(QObject):
self._last_percentage = perc self._last_percentage = perc
@pyqtSlot() @pyqtSlot()
def on_finished(self): def on_reply_finished(self):
"""Clean up when the download was finished.""" """Clean up when the download was finished.
Note when this gets called, only the QNetworkReply has finished. This
doesn't mean the download (i.e. writing data to the disk) is finished
as well. Therefore, we can't close() the QNetworkReply in here yet.
"""
self.bytes_done = self.bytes_total self.bytes_done = self.bytes_total
self.timer.stop() self.timer.stop()
self.fileobj.write(self.reply.readAll()) logger.debug("Reply finished, fileobj {}".format(self.fileobj))
self.fileobj.close() if self.fileobj is None:
self.reply.close() # We'll handle emptying the buffer and cleaning up as soon as the
self.reply.deleteLater() # filename is set.
logger.debug("Download finished") self._do_delayed_write = True
else:
# We can do a "delayed" write immediately to empty the buffer and
# clean up.
self.delayed_write()
@pyqtSlot() @pyqtSlot()
def on_ready_read(self): def on_ready_read(self):
"""Read available data and save file when ready to read.""" """Read available data and save file when ready to read."""
# FIXME exceptions if self.fileobj is None:
# No filename has been set yet, so we don't empty the buffer.
return
try:
self.fileobj.write(self.reply.readAll()) self.fileobj.write(self.reply.readAll())
except OSError as e:
self._die(e.strerror)
@pyqtSlot(int)
def on_reply_error(self, code):
"""Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError:
return
else:
self.error.emit(self.reply.errorString())
@pyqtSlot() @pyqtSlot()
def update_speed(self): def update_speed(self):
@ -145,10 +224,6 @@ class DownloadItem(QObject):
self._last_done = self.bytes_done self._last_done = self.bytes_done
self.speed_changed.emit(self.speed) self.speed_changed.emit(self.speed)
@pyqtSlot(int)
def on_error(self, code):
logger.debug("Error {} in download".format(code))
class DownloadManager(QObject): class DownloadManager(QObject):
@ -213,16 +288,18 @@ class DownloadManager(QObject):
suggested_filename = os.path.join(download_location, suggested_filename = os.path.join(download_location,
suggested_filename) suggested_filename)
logger.debug("fetch: {} -> {}".format(reply.url(), suggested_filename)) logger.debug("fetch: {} -> {}".format(reply.url(), suggested_filename))
filename = message.modular_question("Save file to:", PromptMode.text, download = DownloadItem(reply)
suggested_filename)
if filename is not None:
download = DownloadItem(reply, filename)
download.finished.connect(self.on_finished) download.finished.connect(self.on_finished)
download.percentage_changed.connect(self.on_data_changed) download.percentage_changed.connect(self.on_data_changed)
download.speed_changed.connect(self.on_data_changed) download.speed_changed.connect(self.on_data_changed)
download.error.connect(self.on_error)
self.download_about_to_be_added.emit(len(self.downloads) + 1) self.download_about_to_be_added.emit(len(self.downloads) + 1)
self.downloads.append(download) self.downloads.append(download)
self.download_added.emit() self.download_added.emit()
message.question("Save file to:", mode=PromptMode.text,
handler=download.set_filename,
cancelled_handler=download.cancel,
default=suggested_filename)
@pyqtSlot() @pyqtSlot()
def on_finished(self): def on_finished(self):
@ -235,3 +312,7 @@ class DownloadManager(QObject):
def on_data_changed(self): def on_data_changed(self):
idx = self.downloads.index(self.sender()) idx = self.downloads.index(self.sender())
self.data_changed.emit(idx) self.data_changed.emit(idx)
@pyqtSlot(str)
def on_error(self, msg):
message.error("Download error: {}".format(msg), queue=True)