Slowly download while the user is entering a filename.
Closes #79. Closes #270.
This commit is contained in:
parent
60ef39b4d0
commit
a00dd7b679
@ -19,8 +19,10 @@
|
|||||||
|
|
||||||
"""Download manager."""
|
"""Download manager."""
|
||||||
|
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import shutil
|
||||||
import functools
|
import functools
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
@ -133,6 +135,19 @@ class DownloadItem(QObject):
|
|||||||
|
|
||||||
"""A single download currently running.
|
"""A single download currently running.
|
||||||
|
|
||||||
|
There are multiple ways the data can flow from the QNetworkReply to the
|
||||||
|
disk.
|
||||||
|
|
||||||
|
If the filename/file object is known immediately when starting the
|
||||||
|
download, QNetworkReply's readyRead writes to the target file directly.
|
||||||
|
|
||||||
|
If not, readyRead is ignored and with self._read_timer we periodically read
|
||||||
|
into the self._buffer BytesIO slowly, so some broken servers don't close
|
||||||
|
our connection.
|
||||||
|
|
||||||
|
As soon as we know the file object, we copy self._buffer over and the next
|
||||||
|
readyRead will write to the real file object.
|
||||||
|
|
||||||
Class attributes:
|
Class attributes:
|
||||||
MAX_REDIRECTS: The maximum redirection count.
|
MAX_REDIRECTS: The maximum redirection count.
|
||||||
|
|
||||||
@ -146,6 +161,10 @@ class DownloadItem(QObject):
|
|||||||
_filename: The filename of the download.
|
_filename: The filename of the download.
|
||||||
_reply: The QNetworkReply associated with this download.
|
_reply: The QNetworkReply associated with this download.
|
||||||
_redirects: How many time we were redirected already.
|
_redirects: How many time we were redirected already.
|
||||||
|
_buffer: A BytesIO object to buffer incoming data until we know the
|
||||||
|
target file.
|
||||||
|
_read_timer: A QTimer which reads the QNetworkReply into self._buffer
|
||||||
|
periodically.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
data_changed: The downloads metadata changed.
|
data_changed: The downloads metadata changed.
|
||||||
@ -175,6 +194,10 @@ class DownloadItem(QObject):
|
|||||||
self.stats.updated.connect(self.data_changed)
|
self.stats.updated.connect(self.data_changed)
|
||||||
self.autoclose = True
|
self.autoclose = True
|
||||||
self._reply = None
|
self._reply = None
|
||||||
|
self._buffer = io.BytesIO()
|
||||||
|
self._read_timer = QTimer()
|
||||||
|
self._read_timer.setInterval(500)
|
||||||
|
self._read_timer.timeout.connect(self.on_read_timer_timeout)
|
||||||
self._redirects = 0
|
self._redirects = 0
|
||||||
self.error_msg = None
|
self.error_msg = None
|
||||||
self.basename = '???'
|
self.basename = '???'
|
||||||
@ -219,6 +242,7 @@ class DownloadItem(QObject):
|
|||||||
def _die(self, msg):
|
def _die(self, msg):
|
||||||
"""Abort the download and emit an error."""
|
"""Abort the download and emit an error."""
|
||||||
assert not self.successful
|
assert not self.successful
|
||||||
|
self._read_timer.stop()
|
||||||
self._reply.downloadProgress.disconnect()
|
self._reply.downloadProgress.disconnect()
|
||||||
self._reply.finished.disconnect()
|
self._reply.finished.disconnect()
|
||||||
self._reply.error.disconnect()
|
self._reply.error.disconnect()
|
||||||
@ -243,11 +267,12 @@ class DownloadItem(QObject):
|
|||||||
reply: The QNetworkReply to handle.
|
reply: The QNetworkReply to handle.
|
||||||
"""
|
"""
|
||||||
self._reply = reply
|
self._reply = reply
|
||||||
reply.setReadBufferSize(16 * 1024 * 1024)
|
reply.setReadBufferSize(16 * 1024 * 1024) # 16 MB
|
||||||
reply.downloadProgress.connect(self.stats.on_download_progress)
|
reply.downloadProgress.connect(self.stats.on_download_progress)
|
||||||
reply.finished.connect(self.on_reply_finished)
|
reply.finished.connect(self.on_reply_finished)
|
||||||
reply.error.connect(self.on_reply_error)
|
reply.error.connect(self.on_reply_error)
|
||||||
reply.readyRead.connect(self.on_ready_read)
|
reply.readyRead.connect(self.on_ready_read)
|
||||||
|
self._read_timer.start()
|
||||||
# We could have got signals before we connected slots to them.
|
# We could have got signals before we connected slots to them.
|
||||||
# Here no signals are connected to the DownloadItem yet, so we use a
|
# Here no signals are connected to the DownloadItem yet, so we use a
|
||||||
# singleShot QTimer to emit them after they are connected.
|
# singleShot QTimer to emit them after they are connected.
|
||||||
@ -281,6 +306,7 @@ class DownloadItem(QObject):
|
|||||||
self._reply.abort()
|
self._reply.abort()
|
||||||
self._reply.deleteLater()
|
self._reply.deleteLater()
|
||||||
self._reply = None
|
self._reply = None
|
||||||
|
self._read_timer.stop()
|
||||||
if self.fileobj is not None:
|
if self.fileobj is not None:
|
||||||
self.fileobj.close()
|
self.fileobj.close()
|
||||||
if self._filename is not None and os.path.exists(self._filename):
|
if self._filename is not None and os.path.exists(self._filename):
|
||||||
@ -336,6 +362,11 @@ class DownloadItem(QObject):
|
|||||||
"{}".format(self.fileobj, fileobj))
|
"{}".format(self.fileobj, fileobj))
|
||||||
self.fileobj = fileobj
|
self.fileobj = fileobj
|
||||||
try:
|
try:
|
||||||
|
self._read_timer.stop()
|
||||||
|
log.downloads.debug("buffer: {} bytes".format(self._buffer.tell()))
|
||||||
|
self._buffer.seek(0)
|
||||||
|
shutil.copyfileobj(self._buffer, fileobj)
|
||||||
|
self._buffer.close()
|
||||||
if self._reply.isFinished():
|
if self._reply.isFinished():
|
||||||
# Downloading to the buffer in RAM has already finished so we
|
# Downloading to the buffer in RAM has already finished so we
|
||||||
# write out the data and clean up now.
|
# write out the data and clean up now.
|
||||||
@ -358,6 +389,7 @@ class DownloadItem(QObject):
|
|||||||
self.successful = self._reply.error() == QNetworkReply.NoError
|
self.successful = self._reply.error() == QNetworkReply.NoError
|
||||||
self._reply.close()
|
self._reply.close()
|
||||||
self._reply.deleteLater()
|
self._reply.deleteLater()
|
||||||
|
self._read_timer.stop()
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
log.downloads.debug("Download finished")
|
log.downloads.debug("Download finished")
|
||||||
|
|
||||||
@ -400,6 +432,11 @@ class DownloadItem(QObject):
|
|||||||
else:
|
else:
|
||||||
self._die(self._reply.errorString())
|
self._die(self._reply.errorString())
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def on_read_timer_timeout(self):
|
||||||
|
"""Read some bytes from the QNetworkReply periodically."""
|
||||||
|
self._buffer.write(self._reply.read(1024))
|
||||||
|
|
||||||
def _handle_redirect(self):
|
def _handle_redirect(self):
|
||||||
"""Handle a HTTP redirect.
|
"""Handle a HTTP redirect.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user