diff options
author | kh1 <karsten.heimrich@digia.com> | 2014-01-16 12:55:00 +0100 |
---|---|---|
committer | Karsten Heimrich <karsten.heimrich@digia.com> | 2014-01-22 17:19:53 +0100 |
commit | dab03d22574735c50c8429f1797b3f0a7bd599b0 (patch) | |
tree | ca53f81eef5a0129c00a254cef812fc08da8b782 /src/libs/installer/downloadfiletask.cpp | |
parent | fe92a09482c5abf7f2c56901f2d60e287282f939 (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.cpp | 380 |
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 |