From 4769e8fd93b2b434c6b38d755d1090eca5b62893 Mon Sep 17 00:00:00 2001 From: Sacha Schutz Date: Sat, 13 Feb 2021 13:59:33 +0100 Subject: Add downloader example with QNetworkAccessManager This widget allows to download a file and show a progress bar. Unlike other example on the internet, bytes are read from the readyRead() method and not from finished(). This makes possible to download large file without consuming memory. Pick-to: 6.0 Task-number: PYSIDE-841 Change-Id: Ic314ef1fbc299be6c3636fcb502b3c532d713cfd Reviewed-by: Cristian Maureira-Fredes --- examples/network/downloader/downloader.py | 215 +++++++++++++++++++++++ examples/network/downloader/downloader.pyproject | 3 + 2 files changed, 218 insertions(+) create mode 100644 examples/network/downloader/downloader.py create mode 100644 examples/network/downloader/downloader.pyproject (limited to 'examples') diff --git a/examples/network/downloader/downloader.py b/examples/network/downloader/downloader.py new file mode 100644 index 000000000..538bb8866 --- /dev/null +++ b/examples/network/downloader/downloader.py @@ -0,0 +1,215 @@ +############################################################################# +## +## Copyright (C) 2021 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide6.QtWidgets import ( + QWidget, + QApplication, + QMessageBox, + QLineEdit, + QProgressBar, + QPushButton, + QHBoxLayout, + QVBoxLayout, + QStyle, + QFileDialog, +) +from PySide6.QtCore import QStandardPaths, QUrl, QFile, QSaveFile, QDir, QIODevice, Slot +from PySide6.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkAccessManager +import sys + + +class DownloaderWidget(QWidget): + """A widget to download a http file to a destination file""" + + def __init__(self, parent=None): + super().__init__(parent) + + self.manager = QNetworkAccessManager(self) + self.link_box = QLineEdit() + self.dest_box = QLineEdit() + self.progress_bar = QProgressBar() + + self.start_button = QPushButton("Start") + self.abort_button = QPushButton("Abort") + + self.link_box.setPlaceholderText("Download Link ...") + + self._open_folder_action = self.dest_box.addAction( + qApp.style().standardIcon(QStyle.SP_DirOpenIcon), QLineEdit.TrailingPosition + ) + self._open_folder_action.triggered.connect(self.on_open_folder) + + #  Current QFile + self.file = None + # Current QNetworkReply + self.reply = None + + #  Default http url + self.link_box.setText( + "http://master.qt.io/archive/qt/6.0/6.0.1/single/qt-everywhere-src-6.0.1.zip" + ) + + #  Default destination dir + self.dest_box.setText( + QDir.fromNativeSeparators( + QStandardPaths.writableLocation(QStandardPaths.DownloadLocation) + ) + ) + + # buttons bar layout + hlayout = QHBoxLayout() + hlayout.addStretch() + hlayout.addWidget(self.start_button) + hlayout.addWidget(self.abort_button) + + # main layout + vlayout = QVBoxLayout(self) + vlayout.addWidget(self.link_box) + vlayout.addWidget(self.dest_box) + vlayout.addWidget(self.progress_bar) + vlayout.addStretch() + vlayout.addLayout(hlayout) + + self.resize(300, 100) + + self.start_button.clicked.connect(self.on_start) + self.abort_button.clicked.connect(self.on_abort) + + @Slot() + def on_start(self): + """When user press start button""" + + #  http file + url_file = QUrl(self.link_box.text()) + + # destination file + dest_path = QDir.fromNativeSeparators(self.dest_box.text().strip()) + dest_file = QDir(dest_path).filePath(url_file.fileName()) + + # Ask a question if file already exists + if QFile.exists(dest_file): + ret = QMessageBox.question( + self, + "File exists", + "Do you want to override the file ?", + QMessageBox.Yes | QMessageBox.No, + ) + if ret == QMessageBox.No: + return + + QFile.remove(dest_file) + + self.start_button.setDisabled(True) + # Create the file in write mode to append bytes + self.file = QSaveFile(dest_file) + + if self.file.open(QIODevice.WriteOnly): + + # Start a GET HTTP request + self.reply = self.manager.get(QNetworkRequest(url_file)) + self.reply.downloadProgress.connect(self.on_progress) + self.reply.finished.connect(self.on_finished) + self.reply.readyRead.connect(self.on_ready_read) + self.reply.errorOccurred.connect(self.on_error) + else: + error = self.file.errorString() + print(f"Cannot open device: {error}") + + @Slot() + def on_abort(self): + """When user press abort button""" + if self.reply: + self.reply.abort() + self.progress_bar.setValue(0) + + if self.file: + self.file.cancelWriting() + + self.start_button.setDisabled(False) + + @Slot() + def on_ready_read(self): + """ Get available bytes and store them into the file""" + if self.reply: + if self.reply.error() == QNetworkReply.NoError: + self.file.write(self.reply.readAll()) + + @Slot() + def on_finished(self): + """ Delete reply and close the file""" + if self.reply: + self.reply.deleteLater() + + if self.file: + self.file.commit() + + self.start_button.setDisabled(False) + + @Slot(int, int) + def on_progress(self, bytesReceived: int, bytesTotal: int): + """ Update progress bar""" + self.progress_bar.setRange(0, bytesTotal) + self.progress_bar.setValue(bytesReceived) + + @Slot(QNetworkReply.NetworkError) + def on_error(self, code: QNetworkReply.NetworkError): + """ Show a message if an error happen """ + if self.reply: + QMessageBox.warning(self, "Error Occurred", self.reply.errorString()) + + @Slot() + def on_open_folder(self): + + dir_path = QFileDialog.getExistingDirectory( + self, "Open Directory", QDir.homePath(), QFileDialog.ShowDirsOnly + ) + + if dir_path: + dest_dir = QDir(dir_path) + self.dest_box.setText(QDir.fromNativeSeparators(dest_dir.path())) + + +if __name__ == "__main__": + + app = QApplication(sys.argv) + + w = DownloaderWidget() + w.show() + sys.exit(app.exec_()) diff --git a/examples/network/downloader/downloader.pyproject b/examples/network/downloader/downloader.pyproject new file mode 100644 index 000000000..af1dbb3c2 --- /dev/null +++ b/examples/network/downloader/downloader.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["downloader.py"] +} -- cgit v1.2.3