diff options
Diffstat (limited to 'src/libs/kdtools/filedownloader.cpp')
-rw-r--r-- | src/libs/kdtools/filedownloader.cpp | 1406 |
1 files changed, 1406 insertions, 0 deletions
diff --git a/src/libs/kdtools/filedownloader.cpp b/src/libs/kdtools/filedownloader.cpp new file mode 100644 index 000000000..508cc395d --- /dev/null +++ b/src/libs/kdtools/filedownloader.cpp @@ -0,0 +1,1406 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Klaralvdalens Datakonsult AB (KDAB) +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** 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 The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/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 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "filedownloader_p.h" +#include "filedownloaderfactory.h" +#include "ui_authenticationdialog.h" + +#include "fileutils.h" + +#include <QDialog> +#include <QDir> +#include <QFile> +#include <QtNetwork/QNetworkAccessManager> +#include <QtNetwork/QNetworkProxyFactory> +#include <QPointer> +#include <QUrl> +#include <QTemporaryFile> +#include <QFileInfo> +#include <QThreadPool> +#include <QDebug> +#include <QSslError> +#include <QBasicTimer> +#include <QTimerEvent> + +using namespace KDUpdater; +using namespace QInstaller; + +static double calcProgress(qint64 done, qint64 total) +{ + return total ? (double(done) / double(total)) : 0; +} + + +// -- KDUpdater::FileDownloader + +/*! + \inmodule kdupdater + \class KDUpdater::FileDownloader + \brief The FileDownloader class is the base class for file downloaders used in KDUpdater. + + File downloaders are used by the KDUpdater::Update class to download update files. Each + subclass of FileDownloader can download files from a specific category of sources (such as + \c local, \c ftp, \c http). + + This is an internal class, not a part of the public API. Currently we have the + following subclasses of FileDownloader: + \list + \li HttpDownloader to download files over FTP, HTTP, or HTTPS if Qt is built with SSL. + \li LocalFileDownloader to copy files from the local file system. + \li ResourceFileDownloader to download resource files. + \endlist +*/ + +/*! + \property FileDownloader::autoRemoveDownloadedFile + \brief Whether the downloaded file should be automatically removed after it + is downloaded and the class goes out of scope. +*/ + +/*! + \property FileDownloader::url + \brief The URL to download files from. +*/ + +/*! + \property FileDownloader::scheme + \brief The scheme to use for downloading files. + */ + +/*! + \fn FileDownloader::authenticatorChanged(const QAuthenticator &authenticator) + This signal is emitted when the authenticator changes to \a authenticator. +*/ + +/*! + \fn FileDownloader::canDownload() const = 0 + Returns \c true if the file exists and is readable. +*/ + +/*! + \fn FileDownloader::clone(QObject *parent=0) const = 0 + Clones the local file downloader and assigns it the parent \a parent. +*/ + +/*! + \fn FileDownloader::downloadCanceled() + This signal is emitted if downloading a file is canceled. +*/ + +/*! + \fn FileDownloader::downloadedFileName() const = 0 + Returns the file name of the downloaded file. +*/ + +/*! + \fn FileDownloader::downloadProgress(double progress) + This signal is emitted with the current download \a progress. +*/ + +/*! + \fn FileDownloader::downloadProgress(qint64 bytesReceived, qint64 bytesToReceive) + This signal is emitted with the download progress as the number of received bytes, + \a bytesReceived, and the total size of the file to download, \a bytesToReceive. +*/ + +/*! + \fn FileDownloader::downloadSpeed(qint64 bytesPerSecond) + This signal is emitted with the download speed in bytes per second as \a bytesPerSecond. +*/ + +/*! + \fn FileDownloader::downloadStarted() + This signal is emitted when downloading a file starts. +*/ + +/*! + \fn FileDownloader::downloadStatus(const QString &status) + This signal is emitted with textual representation of the current download \a status in the + following format: "100 MiB of 150 MiB - (DAYS) (HOURS) (MINUTES) (SECONDS) remaining". +*/ + +/*! + \fn FileDownloader::estimatedDownloadTime(int seconds) + This signal is emitted with the estimated download time in \a seconds. +*/ + +/*! + \fn FileDownloader::isDownloaded() const = 0 + Returns \c true if the file is downloaded. +*/ + +/*! + \fn FileDownloader::onError() = 0 + Closes the destination file if an error occurs during copying and stops + the download speed timer. +*/ + +/*! + \fn FileDownloader::onSuccess() = 0 + Closes the destination file after it has been successfully copied and stops + the download speed timer. +*/ + +/*! + \fn FileDownloader::setDownloadedFileName(const QString &name) = 0 + Sets the file name of the downloaded file to \a name. +*/ + +struct KDUpdater::FileDownloader::Private +{ + Private() + : m_hash(QCryptographicHash::Sha1) + , m_assumedSha1Sum("") + , autoRemove(true) + , m_speedTimerInterval(100) + , m_bytesReceived(0) + , m_bytesToReceive(0) + , m_currentSpeedBin(0) + , m_sampleIndex(0) + , m_downloadSpeed(0) + , m_factory(0) + , m_ignoreSslErrors(false) + { + memset(m_samples, 0, sizeof(m_samples)); + } + + ~Private() + { + delete m_factory; + } + + QUrl url; + QString scheme; + + QCryptographicHash m_hash; + QByteArray m_assumedSha1Sum; + + QString errorString; + bool autoRemove; + bool followRedirect; + + QBasicTimer m_timer; + int m_speedTimerInterval; + + qint64 m_bytesReceived; + qint64 m_bytesToReceive; + + mutable qint64 m_samples[50]; + mutable qint64 m_currentSpeedBin; + mutable quint32 m_sampleIndex; + mutable qint64 m_downloadSpeed; + + QAuthenticator m_authenticator; + FileDownloaderProxyFactory *m_factory; + bool m_ignoreSslErrors; +}; + +/*! + Creates a file downloader with the scheme \a scheme and parent \a parent. +*/ +KDUpdater::FileDownloader::FileDownloader(const QString &scheme, QObject *parent) + : QObject(parent) + , d(new Private) +{ + d->scheme = scheme; + d->followRedirect = false; +} + +/*! + Destroys the file downloader. +*/ +KDUpdater::FileDownloader::~FileDownloader() +{ + delete d; +} + +void KDUpdater::FileDownloader::setUrl(const QUrl &url) +{ + d->url = url; +} + +QUrl KDUpdater::FileDownloader::url() const +{ + return d->url; +} + +/*! + Returns the SHA-1 checksum of the downloaded file. +*/ +QByteArray KDUpdater::FileDownloader::sha1Sum() const +{ + return d->m_hash.result(); +} + +/*! + Returns the assumed SHA-1 checksum of the file to download. +*/ +QByteArray KDUpdater::FileDownloader::assumedSha1Sum() const +{ + return d->m_assumedSha1Sum; +} + +/*! + Sets the assumed SHA-1 checksum of the file to download to \a sum. +*/ +void KDUpdater::FileDownloader::setAssumedSha1Sum(const QByteArray &sum) +{ + d->m_assumedSha1Sum = sum; +} + +/*! + Returns an error message. +*/ +QString FileDownloader::errorString() const +{ + return d->errorString; +} + +/*! + Sets the human readable description of the last error that occurred to \a error. Emits the + downloadStatus() and downloadAborted() signals. +*/ +void FileDownloader::setDownloadAborted(const QString &error) +{ + d->errorString = error; + emit downloadStatus(error); + emit downloadAborted(error); +} + +/*! + Sets the download status to \c completed and displays a status message. + + If an assumed SHA-1 checksum is set and the actual calculated checksum does not match it, sets + the status to \c error. If no SHA-1 is assumed, no check is performed, and status is set to + \c success. + + Emits the downloadCompleted() and downloadStatus() signals on success. +*/ +void KDUpdater::FileDownloader::setDownloadCompleted() +{ + if (d->m_assumedSha1Sum.isEmpty() || (d->m_assumedSha1Sum == sha1Sum())) { + onSuccess(); + emit downloadCompleted(); + emit downloadStatus(tr("Download finished.")); + } else { + onError(); + setDownloadAborted(tr("Cryptographic hashes do not match.")); + } +} + +/*! + Emits the downloadCanceled() and downloadStatus() signals. +*/ +void KDUpdater::FileDownloader::setDownloadCanceled() +{ + emit downloadCanceled(); + emit downloadStatus(tr("Download canceled.")); +} + +QString KDUpdater::FileDownloader::scheme() const +{ + return d->scheme; +} + +void KDUpdater::FileDownloader::setScheme(const QString &scheme) +{ + d->scheme = scheme; +} + +void KDUpdater::FileDownloader::setAutoRemoveDownloadedFile(bool val) +{ + d->autoRemove = val; +} + +/*! + Determines that redirects should be followed if \a val is \c true. +*/ +void KDUpdater::FileDownloader::setFollowRedirects(bool val) +{ + d->followRedirect = val; +} + +/*! + Returns whether redirects should be followed. +*/ +bool KDUpdater::FileDownloader::followRedirects() const +{ + return d->followRedirect; +} + +bool KDUpdater::FileDownloader::isAutoRemoveDownloadedFile() const +{ + return d->autoRemove; +} + +/*! + Downloads files. +*/ +void KDUpdater::FileDownloader::download() +{ + QMetaObject::invokeMethod(this, "doDownload", Qt::QueuedConnection); +} + +/*! + Cancels file download. +*/ +void KDUpdater::FileDownloader::cancelDownload() +{ + // Do nothing +} + +/*! + Starts the download speed timer. +*/ +void KDUpdater::FileDownloader::runDownloadSpeedTimer() +{ + if (!d->m_timer.isActive()) + d->m_timer.start(d->m_speedTimerInterval, this); +} + +/*! + Stops the download speed timer. +*/ +void KDUpdater::FileDownloader::stopDownloadSpeedTimer() +{ + d->m_timer.stop(); +} + +/*! + Adds \a sample to the current speed bin. +*/ +void KDUpdater::FileDownloader::addSample(qint64 sample) +{ + d->m_currentSpeedBin += sample; +} + +/*! + Returns the download speed timer ID. +*/ +int KDUpdater::FileDownloader::downloadSpeedTimerId() const +{ + return d->m_timer.timerId(); +} + +/*! + Sets the file download progress to the number of received bytes, \a bytesReceived, + and the number of total bytes to receive, \a bytesToReceive. +*/ +void KDUpdater::FileDownloader::setProgress(qint64 bytesReceived, qint64 bytesToReceive) +{ + d->m_bytesReceived = bytesReceived; + d->m_bytesToReceive = bytesToReceive; +} + +/*! + Calculates the download speed in bytes per second and emits the downloadSpeed() signal. +*/ +void KDUpdater::FileDownloader::emitDownloadSpeed() +{ + unsigned int windowSize = sizeof(d->m_samples) / sizeof(qint64); + + // add speed of last time bin to the window + d->m_samples[d->m_sampleIndex % windowSize] = d->m_currentSpeedBin; + d->m_currentSpeedBin = 0; // reset bin for next time interval + + // advance the sample index + d->m_sampleIndex++; + d->m_downloadSpeed = 0; + + // dynamic window size until the window is completely filled + if (d->m_sampleIndex < windowSize) + windowSize = d->m_sampleIndex; + + for (unsigned int i = 0; i < windowSize; ++i) + d->m_downloadSpeed += d->m_samples[i]; + + d->m_downloadSpeed /= windowSize; // computer average + d->m_downloadSpeed *= 1000.0 / d->m_speedTimerInterval; // rescale to bytes/second + + emit downloadSpeed(d->m_downloadSpeed); +} + +/*! + Builds a textual representation of the download status in the following format: + "100 MiB of 150 MiB - (DAYS) (HOURS) (MINUTES) (SECONDS) remaining". + + Emits the downloadStatus() signal. +*/ +void KDUpdater::FileDownloader::emitDownloadStatus() +{ + QString status; + if (d->m_bytesToReceive > 0) { + QString bytesReceived = humanReadableSize(d->m_bytesReceived); + const QString bytesToReceive = humanReadableSize(d->m_bytesToReceive); + + // remove the unit from the bytesReceived value if bytesToReceive has the same + const QString tmp = bytesToReceive.mid(bytesToReceive.indexOf(QLatin1Char(' '))); + if (bytesReceived.endsWith(tmp)) + bytesReceived.chop(tmp.length()); + + status = tr("%1 of %2").arg(bytesReceived).arg(bytesToReceive); + } else { + if (d->m_bytesReceived > 0) + status = tr("%1 downloaded.").arg(humanReadableSize(d->m_bytesReceived)); + } + + status += QLatin1Char(' ') + tr("(%1/sec)").arg(humanReadableSize(d->m_downloadSpeed)); + if (d->m_bytesToReceive > 0 && d->m_downloadSpeed > 0) { + const qint64 time = (d->m_bytesToReceive - d->m_bytesReceived) / d->m_downloadSpeed; + + int s = time % 60; + const int d = time / 86400; + const int h = (time / 3600) - (d * 24); + const int m = (time / 60) - (d * 1440) - (h * 60); + + QString days; + if (d > 0) + days = tr("%n day(s), ", "", d); + + QString hours; + if (h > 0) + hours = tr("%n hour(s), ", "", h); + + QString minutes; + if (m > 0) + minutes = tr("%n minute(s)", "", m); + + QString seconds; + if (s >= 0 && minutes.isEmpty()) { + s = (s <= 0 ? 1 : s); + seconds = tr("%n second(s)", "", s); + } + status += tr(" - %1%2%3%4 remaining.").arg(days).arg(hours).arg(minutes).arg(seconds); + } else { + status += tr(" - unknown time remaining."); + } + + emit downloadStatus(status); +} + +/*! + Emits dowload progress. +*/ +void KDUpdater::FileDownloader::emitDownloadProgress() +{ + emit downloadProgress(d->m_bytesReceived, d->m_bytesToReceive); +} + +/*! + Emits the estimated download time. +*/ +void KDUpdater::FileDownloader::emitEstimatedDownloadTime() +{ + if (d->m_bytesToReceive <= 0 || d->m_downloadSpeed <= 0) { + emit estimatedDownloadTime(-1); + return; + } + emit estimatedDownloadTime((d->m_bytesToReceive - d->m_bytesReceived) / d->m_downloadSpeed); +} + +/*! + \overload addCheckSumData() +*/ +void KDUpdater::FileDownloader::addCheckSumData(const QByteArray &data) +{ + d->m_hash.addData(data); +} + +/*! + Adds the \a length of characters of \a data to the cryptographic hash of the downloaded file. +*/ +void KDUpdater::FileDownloader::addCheckSumData(const char *data, int length) +{ + d->m_hash.addData(data, length); +} + +/*! + Resets SHA-1 checksum data of the downloaded file. +*/ +void KDUpdater::FileDownloader::resetCheckSumData() +{ + d->m_hash.reset(); +} + + +/*! + Returns a copy of the proxy factory that this FileDownloader object is using to determine the + proxies to be used for requests. +*/ +FileDownloaderProxyFactory *KDUpdater::FileDownloader::proxyFactory() const +{ + if (d->m_factory) + return d->m_factory->clone(); + return 0; +} + +/*! + Sets the proxy factory for this class to be \a factory. A proxy factory is used to determine a + more specific list of proxies to be used for a given request, instead of trying to use the same + proxy value for all requests. This might only be of use for HTTP or FTP requests. +*/ +void KDUpdater::FileDownloader::setProxyFactory(FileDownloaderProxyFactory *factory) +{ + delete d->m_factory; + d->m_factory = factory; +} + +/*! + Returns a copy of the authenticator that this FileDownloader object is using to set the username + and password for a download request. +*/ +QAuthenticator KDUpdater::FileDownloader::authenticator() const +{ + return d->m_authenticator; +} + +/*! + Sets the authenticator object for this class to be \a authenticator. An authenticator is used to + pass on the required authentication information. This might only be of use for HTTP or FTP + requests. Emits the authenticator changed signal with the new authenticator in use. +*/ +void KDUpdater::FileDownloader::setAuthenticator(const QAuthenticator &authenticator) +{ + if (d->m_authenticator.isNull() || (d->m_authenticator != authenticator)) { + d->m_authenticator = authenticator; + emit authenticatorChanged(authenticator); + } +} + +/*! + Returns \c true if SSL errors should be ignored. +*/ +bool KDUpdater::FileDownloader::ignoreSslErrors() +{ + return d->m_ignoreSslErrors; +} + +/*! + Determines that SSL errors should be ignored if \a ignore is \c true. +*/ +void KDUpdater::FileDownloader::setIgnoreSslErrors(bool ignore) +{ + d->m_ignoreSslErrors = ignore; +} + +// -- KDUpdater::LocalFileDownloader + +/*! + \inmodule kdupdater + \class KDUpdater::LocalFileDownloader + \brief The LocalFileDownloader class is used to copy files from the local + file system. + + The user of KDUpdater might be simultaneously downloading several files; + sometimes in parallel to other file downloaders. If copying a local file takes + a long time, it will make the other downloads hang. Therefore, a timer is used + and one block of data is copied per unit time, even though QFile::copy() does the + task of copying local files from one place to another. +*/ + +struct KDUpdater::LocalFileDownloader::Private +{ + Private() + : source(0) + , destination(0) + , downloaded(false) + , timerId(-1) + {} + + QFile *source; + QFile *destination; + QString destFileName; + bool downloaded; + int timerId; +}; + +/*! + Creates a local file downloader with the parent \a parent. +*/ +KDUpdater::LocalFileDownloader::LocalFileDownloader(QObject *parent) + : KDUpdater::FileDownloader(QLatin1String("file"), parent) + , d (new Private) +{ +} + +/*! + Destroys the local file downloader. +*/ +KDUpdater::LocalFileDownloader::~LocalFileDownloader() +{ + if (this->isAutoRemoveDownloadedFile() && !d->destFileName.isEmpty()) + QFile::remove(d->destFileName); + + delete d; +} + +/*! + Returns \c true if the file exists and is readable. +*/ +bool KDUpdater::LocalFileDownloader::canDownload() const +{ + QFileInfo fi(url().toLocalFile()); + return fi.exists() && fi.isReadable(); +} + +/*! + Returns \c true if the file is copied. +*/ +bool KDUpdater::LocalFileDownloader::isDownloaded() const +{ + return d->downloaded; +} + +void KDUpdater::LocalFileDownloader::doDownload() +{ + // Already downloaded + if (d->downloaded) + return; + + // Already started downloading + if (d->timerId >= 0) + return; + + // Open source and destination files + QString localFile = this->url().toLocalFile(); + d->source = new QFile(localFile, this); + if (!d->source->open(QFile::ReadOnly)) { + onError(); + setDownloadAborted(tr("Cannot open file \"%1\" for reading: %2").arg(QFileInfo(localFile) + .fileName(), d->source->errorString())); + return; + } + + if (d->destFileName.isEmpty()) { + QTemporaryFile *file = new QTemporaryFile(this); + file->open(); + d->destination = file; + } else { + d->destination = new QFile(d->destFileName, this); + d->destination->open(QIODevice::ReadWrite | QIODevice::Truncate); + } + + if (!d->destination->isOpen()) { + onError(); + setDownloadAborted(tr("Cannot open file \"%1\" for writing: %2") + .arg(QFileInfo(d->destination->fileName()).fileName(), d->destination->errorString())); + return; + } + + runDownloadSpeedTimer(); + // Start a timer and kickoff the copy process + d->timerId = startTimer(0); // as fast as possible + + emit downloadStarted(); + emit downloadProgress(0); +} + +/*! + Returns the file name of the copied file. +*/ +QString KDUpdater::LocalFileDownloader::downloadedFileName() const +{ + return d->destFileName; +} + +/*! + Sets the file name of the copied file to \a name. +*/ +void KDUpdater::LocalFileDownloader::setDownloadedFileName(const QString &name) +{ + d->destFileName = name; +} + +/*! + Clones the local file downloader and assigns it the parent \a parent. Returns + the new local file downloader. +*/ +KDUpdater::LocalFileDownloader *KDUpdater::LocalFileDownloader::clone(QObject *parent) const +{ + return new LocalFileDownloader(parent); +} + +/*! + Cancels copying the file. +*/ +void KDUpdater::LocalFileDownloader::cancelDownload() +{ + if (d->timerId < 0) + return; + + killTimer(d->timerId); + d->timerId = -1; + + onError(); + setDownloadCanceled(); +} + +/*! + Called when the download timer event \a event occurs. +*/ +void KDUpdater::LocalFileDownloader::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == d->timerId) { + if (!d->source || !d->destination) + return; + + const qint64 blockSize = 32768; + QByteArray buffer; + buffer.resize(blockSize); + const qint64 numRead = d->source->read(buffer.data(), buffer.size()); + qint64 toWrite = numRead; + while (toWrite > 0) { + const qint64 numWritten = d->destination->write(buffer.constData() + numRead - toWrite, toWrite); + if (numWritten < 0) { + killTimer(d->timerId); + d->timerId = -1; + onError(); + setDownloadAborted(tr("Writing to file \"%1\" failed: %2").arg( + QDir::toNativeSeparators(d->destination->fileName()), + d->destination->errorString())); + return; + } + toWrite -= numWritten; + } + addSample(numRead); + addCheckSumData(buffer.data(), numRead); + + if (numRead > 0) { + setProgress(d->source->pos(), d->source->size()); + emit downloadProgress(calcProgress(d->source->pos(), d->source->size())); + return; + } + + d->destination->flush(); + + killTimer(d->timerId); + d->timerId = -1; + + setDownloadCompleted(); + } else if (event->timerId() == downloadSpeedTimerId()) { + emitDownloadSpeed(); + emitDownloadStatus(); + emitDownloadProgress(); + emitEstimatedDownloadTime(); + } +} + +/*! + Closes the destination file after it has been successfully copied and stops + the download speed timer. +*/ +void LocalFileDownloader::onSuccess() +{ + d->downloaded = true; + d->destFileName = d->destination->fileName(); + if (QTemporaryFile *file = dynamic_cast<QTemporaryFile *>(d->destination)) + file->setAutoRemove(false); + d->destination->close(); + delete d->destination; + d->destination = 0; + delete d->source; + d->source = 0; + stopDownloadSpeedTimer(); +} + +/*! + Clears the destination file if an error occurs during copying and stops + the download speed timer. +*/ +void LocalFileDownloader::onError() +{ + d->downloaded = false; + d->destFileName.clear(); + delete d->destination; + d->destination = 0; + delete d->source; + d->source = 0; + stopDownloadSpeedTimer(); +} + + +// -- ResourceFileDownloader + +/*! + \inmodule kdupdater + \class KDUpdater::ResourceFileDownloader + \brief The ResourceFileDownloader class can be used to download resource files. +*/ +struct KDUpdater::ResourceFileDownloader::Private +{ + Private() + : timerId(-1) + , downloaded(false) + {} + + int timerId; + QFile destFile; + bool downloaded; +}; + +/*! + Creates a resource file downloader with the parent \a parent. +*/ +KDUpdater::ResourceFileDownloader::ResourceFileDownloader(QObject *parent) + : KDUpdater::FileDownloader(QLatin1String("resource"), parent) + , d(new Private) +{ +} + +/*! + Destroys the resource file downloader. +*/ +KDUpdater::ResourceFileDownloader::~ResourceFileDownloader() +{ + delete d; +} + +/*! + Returns \c true if the file exists and is readable. +*/ +bool KDUpdater::ResourceFileDownloader::canDownload() const +{ + const QFileInfo fi(QInstaller::pathFromUrl(url())); + return fi.exists() && fi.isReadable(); +} + +/*! + Returns \c true if the file is downloaded. +*/ +bool KDUpdater::ResourceFileDownloader::isDownloaded() const +{ + return d->downloaded; +} + +/*! + Downloads a resource file. +*/ +void KDUpdater::ResourceFileDownloader::doDownload() +{ + // Already downloaded + if (d->downloaded) + return; + + // Already started downloading + if (d->timerId >= 0) + return; + + // Open source and destination files + QUrl url = this->url(); + url.setScheme(QString::fromLatin1("file")); + d->destFile.setFileName(QString::fromLatin1(":%1").arg(url.toLocalFile())); + + emit downloadStarted(); + emit downloadProgress(0); + + d->destFile.open(QIODevice::ReadOnly); + d->timerId = startTimer(0); // start as fast as possible +} + +/*! + Returns the file name of the downloaded file. +*/ +QString KDUpdater::ResourceFileDownloader::downloadedFileName() const +{ + return d->destFile.fileName(); +} + +/*! + Sets the file name of the downloaded file to \a name. +*/ +void KDUpdater::ResourceFileDownloader::setDownloadedFileName(const QString &/*name*/) +{ + // Not supported! +} + +/*! + Clones the resource file downloader and assigns it the parent \a parent. Returns + the new resource file downloader. +*/ +KDUpdater::ResourceFileDownloader *KDUpdater::ResourceFileDownloader::clone(QObject *parent) const +{ + return new ResourceFileDownloader(parent); +} + +/*! + Cancels downloading the file. +*/ +void KDUpdater::ResourceFileDownloader::cancelDownload() +{ + if (d->timerId < 0) + return; + + killTimer(d->timerId); + d->timerId = -1; + + setDownloadCanceled(); +} + +/*! + Called when the download timer event \a event occurs. +*/ +void KDUpdater::ResourceFileDownloader::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == d->timerId) { + if (!d->destFile.isOpen()) { + onError(); + killTimer(d->timerId); + emit downloadProgress(1); + setDownloadAborted(tr("Cannot read resource file \"%1\": %2").arg(downloadedFileName(), + d->destFile.errorString())); + return; + } + + QByteArray buffer; + buffer.resize(32768); + const qint64 numRead = d->destFile.read(buffer.data(), buffer.size()); + + addSample(numRead); + addCheckSumData(buffer.data(), numRead); + + if (numRead > 0) { + setProgress(d->destFile.pos(), d->destFile.size()); + emit downloadProgress(calcProgress(d->destFile.pos(), d->destFile.size())); + return; + } + + killTimer(d->timerId); + d->timerId = -1; + setDownloadCompleted(); + } else if (event->timerId() == downloadSpeedTimerId()) { + emitDownloadSpeed(); + emitDownloadStatus(); + emitDownloadProgress(); + emitEstimatedDownloadTime(); + } +} + +/*! + Closes the destination file after it has been successfully copied and stops + the download speed timer. +*/ +void KDUpdater::ResourceFileDownloader::onSuccess() +{ + d->destFile.close(); + d->downloaded = true; + stopDownloadSpeedTimer(); +} + +/*! + Closes the destination file if an error occurs during copying and stops + the download speed timer. +*/ +void KDUpdater::ResourceFileDownloader::onError() +{ + d->destFile.close(); + d->downloaded = false; + stopDownloadSpeedTimer(); + d->destFile.setFileName(QString()); +} + + +// -- KDUpdater::HttpDownloader + +/*! + \inmodule kdupdater + \class KDUpdater::HttpDownloader + \brief The HttpDownloader class is used to download files over FTP, HTTP, or HTTPS. + + HTTPS is supported if Qt is built with SSL. +*/ +struct KDUpdater::HttpDownloader::Private +{ + explicit Private(HttpDownloader *qq) + : q(qq) + , http(0) + , destination(0) + , downloaded(false) + , aborted(false) + , m_authenticationCount(0) + {} + + HttpDownloader *const q; + QNetworkAccessManager manager; + QNetworkReply *http; + QFile *destination; + QString destFileName; + bool downloaded; + bool aborted; + int m_authenticationCount; + + void shutDown() + { + disconnect(http, &QNetworkReply::finished, q, &HttpDownloader::httpReqFinished); + http->deleteLater(); + http = 0; + destination->close(); + destination->deleteLater(); + destination = 0; + q->resetCheckSumData(); + } +}; + +/*! + Creates an HTTP downloader with the parent \a parent. +*/ +KDUpdater::HttpDownloader::HttpDownloader(QObject *parent) + : KDUpdater::FileDownloader(QLatin1String("http"), parent) + , d(new Private(this)) +{ +#ifndef QT_NO_SSL + connect(&d->manager, &QNetworkAccessManager::sslErrors, + this, &HttpDownloader::onSslErrors); +#endif + connect(&d->manager, &QNetworkAccessManager::authenticationRequired, + this, &HttpDownloader::onAuthenticationRequired); +} + +/*! + Destroys an HTTP downloader. + + Removes the downloaded file if FileDownloader::isAutoRemoveDownloadedFile() returns \c true or + FileDownloader::setAutoRemoveDownloadedFile() was called with \c true. +*/ +KDUpdater::HttpDownloader::~HttpDownloader() +{ + if (this->isAutoRemoveDownloadedFile() && !d->destFileName.isEmpty()) + QFile::remove(d->destFileName); + delete d; +} + +/*! + Returns \c true if the file exists and is readable. +*/ +bool KDUpdater::HttpDownloader::canDownload() const +{ + // TODO: Check whether the http file actually exists or not. + return true; +} + +/*! + Returns \c true if the file is downloaded. +*/ +bool KDUpdater::HttpDownloader::isDownloaded() const +{ + return d->downloaded; +} + +void KDUpdater::HttpDownloader::doDownload() +{ + if (d->downloaded) + return; + + if (d->http) + return; + + startDownload(url()); + runDownloadSpeedTimer(); +} + +/*! + Returns the file name of the downloaded file. +*/ +QString KDUpdater::HttpDownloader::downloadedFileName() const +{ + return d->destFileName; +} + +/*! + Sets the file name of the downloaded file to \a name. +*/ +void KDUpdater::HttpDownloader::setDownloadedFileName(const QString &name) +{ + d->destFileName = name; +} + +/*! + Clones the HTTP downloader and assigns it the parent \a parent. Returns the new + HTTP downloader. +*/ +KDUpdater::HttpDownloader *KDUpdater::HttpDownloader::clone(QObject *parent) const +{ + return new HttpDownloader(parent); +} + +void KDUpdater::HttpDownloader::httpReadyRead() +{ + static QByteArray buffer(16384, '\0'); + while (d->http->bytesAvailable()) { + const qint64 read = d->http->read(buffer.data(), buffer.size()); + qint64 written = 0; + while (written < read) { + const qint64 numWritten = d->destination->write(buffer.data() + written, read - written); + if (numWritten < 0) { + const QString error = d->destination->errorString(); + const QString fileName = d->destination->fileName(); + d->shutDown(); + setDownloadAborted(tr("Cannot download %1. Writing to file \"%2\" failed: %3") + .arg(url().toString(), fileName, error)); + return; + } + written += numWritten; + } + addSample(written); + addCheckSumData(buffer.data(), read); + } +} + +void KDUpdater::HttpDownloader::httpError(QNetworkReply::NetworkError) +{ + if (!d->aborted) + httpDone(true); +} + +/*! + Cancels downloading the file. +*/ +void KDUpdater::HttpDownloader::cancelDownload() +{ + d->aborted = true; + if (d->http) { + d->http->abort(); + httpDone(true); + } +} + +void KDUpdater::HttpDownloader::httpDone(bool error) +{ + if (error) { + QString err; + if (d->http) { + err = d->http->errorString(); + d->http->deleteLater(); + d->http = 0; + onError(); + } + + if (d->aborted) { + d->aborted = false; + setDownloadCanceled(); + } else { + setDownloadAborted(err); + } + } + //PENDING: what about the non-error case?? +} + +/*! + Closes the destination file if an error occurs during copying and stops + the download speed timer. +*/ +void KDUpdater::HttpDownloader::onError() +{ + d->downloaded = false; + d->destFileName.clear(); + delete d->destination; + d->destination = 0; + stopDownloadSpeedTimer(); +} + +/*! + Closes the destination file after it has been successfully copied and stops + the download speed timer. +*/ +void KDUpdater::HttpDownloader::onSuccess() +{ + d->downloaded = true; + d->destFileName = d->destination->fileName(); + if (QTemporaryFile *file = dynamic_cast<QTemporaryFile *>(d->destination)) + file->setAutoRemove(false); + delete d->destination; + d->destination = 0; + stopDownloadSpeedTimer(); +} + +void KDUpdater::HttpDownloader::httpReqFinished() +{ + const QVariant redirect = d->http == 0 ? QVariant() + : d->http->attribute(QNetworkRequest::RedirectionTargetAttribute); + + const QUrl redirectUrl = redirect.toUrl(); + if (followRedirects() && redirectUrl.isValid()) { + d->shutDown(); // clean the previous download + startDownload(redirectUrl); + } else { + if (d->http == 0) + return; + + httpReadyRead(); + d->destination->flush(); + setDownloadCompleted(); + d->http->deleteLater(); + d->http = 0; + } +} + +void KDUpdater::HttpDownloader::httpReadProgress(qint64 done, qint64 total) +{ + if (d->http) { + const QUrl redirectUrl = d->http->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + if (followRedirects() && redirectUrl.isValid()) + return; // if we are a redirection, do not emit the progress + } + + setProgress(done, total); + emit downloadProgress(calcProgress(done, total)); +} + +/*! + Called when the download timer event \a event occurs. +*/ +void KDUpdater::HttpDownloader::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == downloadSpeedTimerId()) { + emitDownloadSpeed(); + emitDownloadStatus(); + emitDownloadProgress(); + emitEstimatedDownloadTime(); + } +} + +void KDUpdater::HttpDownloader::startDownload(const QUrl &url) +{ + d->m_authenticationCount = 0; + d->manager.setProxyFactory(proxyFactory()); + d->http = d->manager.get(QNetworkRequest(url)); + + connect(d->http, &QIODevice::readyRead, this, &HttpDownloader::httpReadyRead); + connect(d->http, &QNetworkReply::downloadProgress, + this, &HttpDownloader::httpReadProgress); + connect(d->http, &QNetworkReply::finished, this, &HttpDownloader::httpReqFinished); + void (QNetworkReply::*errorSignal)(QNetworkReply::NetworkError) = &QNetworkReply::error; + connect(d->http, errorSignal, this, &HttpDownloader::httpError); + + if (d->destFileName.isEmpty()) { + QTemporaryFile *file = new QTemporaryFile(this); + file->open(); + d->destination = file; + } else { + d->destination = new QFile(d->destFileName, this); + d->destination->open(QIODevice::ReadWrite | QIODevice::Truncate); + } + + if (!d->destination->isOpen()) { + const QString error = d->destination->errorString(); + const QString fileName = d->destination->fileName(); + d->shutDown(); + setDownloadAborted(tr("Cannot download %1. Cannot create file \"%2\": %3").arg( + url.toString(), fileName, error)); + } +} + +void KDUpdater::HttpDownloader::onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator) +{ + Q_UNUSED(reply) + // first try with the information we have already + if (d->m_authenticationCount == 0) { + d->m_authenticationCount++; + authenticator->setUser(this->authenticator().user()); + authenticator->setPassword(this->authenticator().password()); + } else if (d->m_authenticationCount == 1) { + // we failed to authenticate, ask for new credentials + QDialog dlg; + Ui::Dialog ui; + ui.setupUi(&dlg); + dlg.adjustSize(); + ui.siteDescription->setText(tr("%1 at %2").arg(authenticator->realm()).arg(url().host())); + + ui.userEdit->setText(this->authenticator().user()); + ui.passwordEdit->setText(this->authenticator().password()); + + if (dlg.exec() == QDialog::Accepted) { + authenticator->setUser(ui.userEdit->text()); + authenticator->setPassword(ui.passwordEdit->text()); + + // update the authenticator we used initially + QAuthenticator auth; + auth.setUser(ui.userEdit->text()); + auth.setPassword(ui.passwordEdit->text()); + emit authenticatorChanged(auth); + } else { + d->shutDown(); + setDownloadAborted(tr("Authentication request canceled.")); + emit downloadCanceled(); + } + d->m_authenticationCount++; + } +} + +#ifndef QT_NO_SSL + +#include "messageboxhandler.h" + +void KDUpdater::HttpDownloader::onSslErrors(QNetworkReply* reply, const QList<QSslError> &errors) +{ + Q_UNUSED(reply) + QString errorString; + foreach (const QSslError &error, errors) { + if (!errorString.isEmpty()) + errorString += QLatin1String(", "); + errorString += error.errorString(); + } + qDebug() << errorString; + + const QStringList arguments = QCoreApplication::arguments(); + if (arguments.contains(QLatin1String("--script")) || arguments.contains(QLatin1String("Script")) + || ignoreSslErrors()) { + reply->ignoreSslErrors(); + return; + } + // TODO: Remove above code once we have a proper implementation for message box handler supporting + // methods used in the following code, right now we return here cause the message box is not scriptable. + + QMessageBox msgBox(MessageBoxHandler::currentBestSuitParent()); + msgBox.setDetailedText(errorString); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setWindowModality(Qt::WindowModal); + msgBox.setWindowTitle(tr("Secure Connection Failed")); + msgBox.setText(tr("There was an error during connection to: %1.").arg(url().toString())); + msgBox.setInformativeText(QString::fromLatin1("<ul><li>%1</li><li>%2</li></ul>").arg(tr("This could be " + "a problem with the server's configuration, or it could be someone trying to impersonate the " + "server."), tr("If you have connected to this server successfully in the past or trust this server, " + "the error may be temporary and you can try again."))); + + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBox.setButtonText(QMessageBox::Yes, tr("Try again")); + msgBox.setDefaultButton(QMessageBox::Cancel); + + if (msgBox.exec() == QMessageBox::Cancel) { + if (!d->aborted) + httpDone(true); + } else { + reply->ignoreSslErrors(); + KDUpdater::FileDownloaderFactory::instance().setIgnoreSslErrors(true); + } +} +#endif |