summaryrefslogtreecommitdiffstats
path: root/src/libs/installer/downloadfiletask.cpp
diff options
context:
space:
mode:
authorkh1 <karsten.heimrich@digia.com>2014-01-16 12:55:00 +0100
committerKarsten Heimrich <karsten.heimrich@digia.com>2014-01-22 17:19:53 +0100
commitdab03d22574735c50c8429f1797b3f0a7bd599b0 (patch)
treeca53f81eef5a0129c00a254cef812fc08da8b782 /src/libs/installer/downloadfiletask.cpp
parentfe92a09482c5abf7f2c56901f2d60e287282f939 (diff)
QFuture based asynchronous Task implementation.
Change-Id: I538a2fc40b67d6d27f120afe3705065ab98f8f99 Reviewed-by: Tim Jenssen <tim.jenssen@digia.com>
Diffstat (limited to 'src/libs/installer/downloadfiletask.cpp')
-rw-r--r--src/libs/installer/downloadfiletask.cpp380
1 files changed, 380 insertions, 0 deletions
diff --git a/src/libs/installer/downloadfiletask.cpp b/src/libs/installer/downloadfiletask.cpp
new file mode 100644
index 000000000..3e1c903d2
--- /dev/null
+++ b/src/libs/installer/downloadfiletask.cpp
@@ -0,0 +1,380 @@
+/**************************************************************************
+**
+** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Installer Framework.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+**************************************************************************/
+#include "downloadfiletask.h"
+
+#include "downloadfiletask_p.h"
+#include "observer.h"
+
+#include <QEventLoop>
+#include <QFile>
+#include <QNetworkProxyFactory>
+#include <QSslError>
+#include <QTemporaryFile>
+#include <QTimer>
+
+namespace QInstaller {
+
+Downloader::Downloader()
+ : m_finished(0)
+{
+ connect(&m_nam, SIGNAL(finished(QNetworkReply*)), SLOT(onFinished(QNetworkReply*)));
+}
+
+Downloader::~Downloader()
+{
+ m_nam.disconnect();
+ foreach (QNetworkReply *const reply, m_downloads.keys()) {
+ reply->disconnect();
+ reply->abort();
+ reply->deleteLater();
+ }
+
+ foreach (const Data &data, m_downloads.values()) {
+ data.file->close();
+ delete data.file;
+ delete data.observer;
+ }
+}
+
+void Downloader::download(QFutureInterface<FileTaskResult> &fi, const QList<FileTaskItem> &items,
+ const QAuthenticator &authenticator, QNetworkProxyFactory *networkProxyFactory)
+{
+ m_items = items;
+ m_futureInterface = &fi;
+ m_authenticator = authenticator;
+
+ fi.reportStarted();
+ fi.setExpectedResultCount(items.count());
+
+ m_nam.setProxyFactory(networkProxyFactory);
+ connect(&m_nam, SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), this,
+ SLOT(onAuthenticationRequired(QNetworkReply*, QAuthenticator*)));
+ QTimer::singleShot(0, this, SLOT(doDownload()));
+}
+
+void Downloader::doDownload()
+{
+ foreach (const FileTaskItem &item, m_items) {
+ if (!startDownload(item))
+ break;
+ }
+
+ if (m_items.isEmpty() || m_futureInterface->isCanceled()) {
+ m_futureInterface->reportFinished();
+ emit finished(); // emit finished, so the event loop can shutdown
+ }
+}
+
+
+// -- private slots
+
+void Downloader::onReadyRead()
+{
+ if (testCanceled()) {
+ m_futureInterface->reportFinished();
+ emit finished(); return; // error
+ }
+
+ QNetworkReply *const reply = qobject_cast<QNetworkReply *>(sender());
+ if (!reply)
+ return;
+
+ const Data &data = m_downloads[reply];
+ if (!data.file->isOpen()) {
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Target '%1' not "
+ "open for write. Error: %2.").arg(data.file->fileName(), data.file->errorString())));
+ return;
+ }
+
+ QByteArray buffer(32768, Qt::Uninitialized);
+ while (reply->bytesAvailable()) {
+ if (testCanceled()) {
+ m_futureInterface->reportFinished();
+ emit finished(); return; // error
+ }
+
+ const qint64 read = reply->read(buffer.data(), buffer.size());
+ qint64 written = 0;
+ while (written < read) {
+ const qint64 toWrite = data.file->write(buffer.constData() + written, read - written);
+ if (toWrite < 0) {
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Writing "
+ "to target '%1' failed. Error: %2.").arg(data.file->fileName(),
+ data.file->errorString())));
+ return;
+ }
+ written += toWrite;
+ }
+
+ data.observer->addSample(read);
+ data.observer->addBytesTransfered(read);
+ data.observer->addCheckSumData(buffer.data(), read);
+
+ int progress = m_finished * 100;
+ foreach (const Data &data, m_downloads.values())
+ progress += data.observer->progressValue();
+ if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
+ m_futureInterface->setProgressValueAndText(progress / m_items.count(),
+ data.observer->progressText());
+ }
+ }
+}
+
+void Downloader::onFinished(QNetworkReply *reply)
+{
+ const Data &data = m_downloads[reply];
+ const QString filename = data.file->fileName();
+ if (!m_futureInterface->isCanceled()) {
+ if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
+ const QUrl url = reply->url()
+ .resolved(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
+ const QList<QUrl> redirects = m_redirects.values(reply);
+ if (!redirects.contains(url)) {
+ data.file->close();
+ data.file->remove();
+ delete data.file;
+ delete data.observer;
+
+ FileTaskItem taskItem(url.toString(), filename);
+ taskItem.insert(FileTaskRole::Checksum, data.expectedCheckSum);
+ QNetworkReply *const redirectReply = startDownload(taskItem);
+
+ foreach (const QUrl &redirect, redirects)
+ m_redirects.insertMulti(redirectReply, redirect);
+ m_redirects.insertMulti(redirectReply, url);
+
+ m_downloads.remove(reply);
+ m_redirects.remove(reply);
+ reply->deleteLater();
+ return;
+ } else {
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Redirect"
+ " loop detected '%1'.").arg(url.toString())));
+ return;
+ }
+ }
+ }
+
+ const QByteArray ba = reply->readAll();
+ if (!ba.isEmpty()) {
+ data.observer->addSample(ba.size());
+ data.observer->addBytesTransfered(ba.size());
+ data.observer->addCheckSumData(ba.data(), ba.size());
+ }
+ m_futureInterface->reportResult(FileTaskResult(filename, data.observer->checkSum()));
+
+ if (!data.expectedCheckSum.isEmpty()) {
+ if (data.expectedCheckSum != data.observer->checkSum().toHex()) {
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Checksum"
+ " mismatch detected '%1'.").arg(reply->url().toString())));
+ }
+ }
+
+ data.file->close();
+ delete data.file;
+ delete data.observer;
+
+ m_downloads.remove(reply);
+ m_redirects.remove(reply);
+ reply->deleteLater();
+
+ m_finished++;
+ if (m_downloads.isEmpty() || m_futureInterface->isCanceled()) {
+ m_futureInterface->reportFinished();
+ emit finished(); // emit finished, so the event loop can shutdown
+ }
+}
+
+void Downloader::onError(QNetworkReply::NetworkError error)
+{
+ QNetworkReply *const reply = qobject_cast<QNetworkReply *>(sender());
+ if (reply) {
+ const Data &data = m_downloads[reply];
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Network error "
+ "while downloading target '%1'. Error: %2.").arg(data.file->fileName(),
+ reply->errorString())));
+ } else {
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Unknown network "
+ "error while downloading. Error: %1.").arg(error)));
+ }
+}
+
+void Downloader::onSslErrors(const QList<QSslError> &sslErrors)
+{
+#if defined(QT_NO_SSL) || defined(QT_NO_OPENSSL)
+ Q_UNUSED(sslErrors);
+#else
+ foreach (const QSslError &error, sslErrors)
+ qDebug() << QString::fromLatin1("SSL error: %s").arg(error.errorString());
+#endif
+}
+
+void Downloader::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ Q_UNUSED(bytesReceived)
+ QNetworkReply *const reply = qobject_cast<QNetworkReply *>(sender());
+ if (reply) {
+ const Data &data = m_downloads[reply];
+ data.observer->setBytesToTransfer(bytesTotal);
+ }
+}
+
+void Downloader::onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator)
+{
+ Q_UNUSED(reply)
+ if (!authenticator)
+ return;
+
+ if (!m_authenticator.user().isEmpty()) {
+ authenticator->setUser(m_authenticator.user());
+ authenticator->setPassword(m_authenticator.password());
+ m_authenticator = QAuthenticator(); // clear so we fail on next call
+ } else {
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Could not "
+ "authenticate using the provided credentials. Source: '%1'.").arg(reply->url()
+ .toString())));
+ }
+}
+
+
+// -- private
+
+bool Downloader::testCanceled()
+{
+ // TODO: figure out how to implement pause and resume
+ if (m_futureInterface->isPaused()) {
+ m_futureInterface->togglePaused(); // Note: this will trigger cancel
+ m_futureInterface->reportException(FileTaskException(QLatin1String("Pause and resume not "
+ "supported by network transfers.")));
+ }
+ return m_futureInterface->isCanceled();
+}
+
+QNetworkReply *Downloader::startDownload(const FileTaskItem &item)
+{
+ QUrl const source = item.source();
+ if (!source.isValid()) {
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Invalid source "
+ "'%1'. Error: %2.").arg(source.toString(), source.errorString())));
+ return 0;
+ }
+
+ QScopedPointer<QFile> file;
+ const QString target = item.target();
+ if (target.isEmpty()) {
+ QTemporaryFile *tmp = new QTemporaryFile;
+ tmp->setAutoRemove(false);
+ file.reset(tmp);
+ } else {
+ file.reset(new QFile(target));
+ }
+ if (!file->open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ file->remove();
+ m_futureInterface->reportException(FileTaskException(QString::fromLatin1("Could not open "
+ "target '%1' for write. Error: %2.").arg(file->fileName(), file->errorString())));
+ return 0;
+ }
+
+ QNetworkReply *reply = m_nam.get(QNetworkRequest(source));
+ m_downloads.insert(reply, Data(file.take(), new FileTaskObserver(QCryptographicHash::Sha1),
+ item.value(FileTaskRole::Checksum).toByteArray()));
+
+ connect(reply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
+ connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this,
+ SLOT(onError(QNetworkReply::NetworkError)));
+#ifndef QT_NO_SSL
+ connect(reply, SIGNAL(sslErrors(QList<QSslError>)), SLOT(onSslErrors(QList<QSslError>)));
+#endif
+ connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onDownloadProgress(qint64,
+ qint64)));
+ return reply;
+}
+
+
+// -- DownloadFileTask
+
+DownloadFileTask::DownloadFileTask(const QList<FileTaskItem> &items)
+ : AbstractFileTask()
+{
+ setTaskItems(items);
+}
+
+void DownloadFileTask::setTaskItem(const FileTaskItem &item)
+{
+ AbstractFileTask::setTaskItem(item);
+}
+
+void DownloadFileTask::addTaskItem(const FileTaskItem &item)
+{
+ AbstractFileTask::addTaskItem(item);
+}
+
+void DownloadFileTask::setTaskItems(const QList<FileTaskItem> &items)
+{
+ AbstractFileTask::setTaskItems(items);
+}
+
+void DownloadFileTask::addTaskItems(const QList<FileTaskItem> &items)
+{
+ AbstractFileTask::addTaskItems(items);
+}
+
+void DownloadFileTask::setAuthenticator(const QAuthenticator &authenticator)
+{
+ m_authenticator = authenticator;
+}
+
+void DownloadFileTask::setProxyFactory(KDUpdater::FileDownloaderProxyFactory *factory)
+{
+ m_proxyFactory.reset(factory);
+}
+
+void DownloadFileTask::doTask(QFutureInterface<FileTaskResult> &fi)
+{
+ QEventLoop el;
+ Downloader downloader;
+ connect(&downloader, SIGNAL(finished()), &el, SLOT(quit()));
+ downloader.download(fi, taskItems(), m_authenticator, (m_proxyFactory.isNull() ? 0
+ : m_proxyFactory->clone())); // Downloader takes ownership of this copy.
+ el.exec(); // That's tricky here, we run our own event loop to keep QNAM working
+}
+
+} // namespace QInstaller