Handle download errors and handle everything async
This commit is contained in:
parent
c91dced99f
commit
ad7856569f
@ -20,6 +20,7 @@
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QTimer
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
import qutebrowser.config.config as config
|
||||
import qutebrowser.utils.message as message
|
||||
@ -51,39 +52,53 @@ class DownloadItem(QObject):
|
||||
percentage_changed: The download percentage changed.
|
||||
arg: The new percentage, -1 if unknown.
|
||||
finished: The download was finished.
|
||||
error: An error with the download occured.
|
||||
arg: The error message as string.
|
||||
"""
|
||||
|
||||
REFRESH_INTERVAL = 200
|
||||
speed_changed = pyqtSignal(float)
|
||||
percentage_changed = pyqtSignal(int)
|
||||
finished = pyqtSignal()
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, reply, filename, parent=None):
|
||||
def __init__(self, reply, parent=None):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
reply: The QNetworkReply to download.
|
||||
filename: The full filename to save the download to.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.reply = reply
|
||||
self.bytes_done = None
|
||||
self.bytes_total = None
|
||||
self.speed = None
|
||||
self.fileobj = None
|
||||
self._do_delayed_write = False
|
||||
self._last_done = None
|
||||
self._last_percentage = None
|
||||
# FIXME exceptions
|
||||
self.fileobj = open(filename, 'wb')
|
||||
reply.setReadBufferSize(16 * 1024 * 1024)
|
||||
reply.downloadProgress.connect(self.on_download_progress)
|
||||
reply.finished.connect(self.on_finished)
|
||||
reply.finished.connect(self.finished)
|
||||
reply.error.connect(self.on_error)
|
||||
reply.finished.connect(self.on_reply_finished)
|
||||
reply.error.connect(self.on_reply_error)
|
||||
reply.readyRead.connect(self.on_ready_read)
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self.update_speed)
|
||||
self.timer.setInterval(self.REFRESH_INTERVAL)
|
||||
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
|
||||
def percentage(self):
|
||||
"""Property to get the current download percentage."""
|
||||
@ -96,6 +111,48 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
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)
|
||||
def on_download_progress(self, bytes_done, bytes_total):
|
||||
"""Upload local variables when the download progress changed.
|
||||
@ -113,21 +170,43 @@ class DownloadItem(QObject):
|
||||
self._last_percentage = perc
|
||||
|
||||
@pyqtSlot()
|
||||
def on_finished(self):
|
||||
"""Clean up when the download was finished."""
|
||||
def on_reply_finished(self):
|
||||
"""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.timer.stop()
|
||||
self.fileobj.write(self.reply.readAll())
|
||||
self.fileobj.close()
|
||||
self.reply.close()
|
||||
self.reply.deleteLater()
|
||||
logger.debug("Download finished")
|
||||
logger.debug("Reply finished, fileobj {}".format(self.fileobj))
|
||||
if self.fileobj is None:
|
||||
# We'll handle emptying the buffer and cleaning up as soon as the
|
||||
# filename is set.
|
||||
self._do_delayed_write = True
|
||||
else:
|
||||
# We can do a "delayed" write immediately to empty the buffer and
|
||||
# clean up.
|
||||
self.delayed_write()
|
||||
|
||||
@pyqtSlot()
|
||||
def on_ready_read(self):
|
||||
"""Read available data and save file when ready to read."""
|
||||
# FIXME exceptions
|
||||
self.fileobj.write(self.reply.readAll())
|
||||
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())
|
||||
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()
|
||||
def update_speed(self):
|
||||
@ -145,10 +224,6 @@ class DownloadItem(QObject):
|
||||
self._last_done = self.bytes_done
|
||||
self.speed_changed.emit(self.speed)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def on_error(self, code):
|
||||
logger.debug("Error {} in download".format(code))
|
||||
|
||||
|
||||
class DownloadManager(QObject):
|
||||
|
||||
@ -213,16 +288,18 @@ class DownloadManager(QObject):
|
||||
suggested_filename = os.path.join(download_location,
|
||||
suggested_filename)
|
||||
logger.debug("fetch: {} -> {}".format(reply.url(), suggested_filename))
|
||||
filename = message.modular_question("Save file to:", PromptMode.text,
|
||||
suggested_filename)
|
||||
if filename is not None:
|
||||
download = DownloadItem(reply, filename)
|
||||
download.finished.connect(self.on_finished)
|
||||
download.percentage_changed.connect(self.on_data_changed)
|
||||
download.speed_changed.connect(self.on_data_changed)
|
||||
self.download_about_to_be_added.emit(len(self.downloads) + 1)
|
||||
self.downloads.append(download)
|
||||
self.download_added.emit()
|
||||
download = DownloadItem(reply)
|
||||
download.finished.connect(self.on_finished)
|
||||
download.percentage_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.downloads.append(download)
|
||||
self.download_added.emit()
|
||||
message.question("Save file to:", mode=PromptMode.text,
|
||||
handler=download.set_filename,
|
||||
cancelled_handler=download.cancel,
|
||||
default=suggested_filename)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_finished(self):
|
||||
@ -235,3 +312,7 @@ class DownloadManager(QObject):
|
||||
def on_data_changed(self):
|
||||
idx = self.downloads.index(self.sender())
|
||||
self.data_changed.emit(idx)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_error(self, msg):
|
||||
message.error("Download error: {}".format(msg), queue=True)
|
||||
|
Loading…
Reference in New Issue
Block a user