view ui/downloader.cpp @ 389:3be838c3e4d8

Handle installation success. Save / load last installed list. This also adds a feature that you can specifiy the download resource when release build is not set so that you can update a list without modifying the server data.
author Andre Heinecke <andre.heinecke@intevation.de>
date Tue, 15 Apr 2014 19:05:49 +0200
parents 63b79d135631
children 17e1c8f37d72
line wrap: on
line source
#include "downloader.h"

#ifndef DOWNLOAD_SERVER
#define DOWNLOAD_SERVER "https://www.intevation.de"
#endif

#include <QFile>
#include <QDir>
#include <QDebug>
#include <QStandardPaths>
#include <QLocale>
#include <QSaveFile>

#include <polarssl/net.h>
#include <polarssl/ssl.h>
#include <polarssl/entropy.h>
#include <polarssl/ctr_drbg.h>
#include <polarssl/error.h>
#include <polarssl/certs.h>

#define MAX_SW_SIZE 10485760
#define MAX_LIST_SIZE 1048576


Downloader::Downloader(QObject* parent, const QString& url,
                       const QByteArray& certificate,
                       const QDateTime& newestSW,
                       const QDateTime& newestList,
                       const QString& resourceSW,
                       const QString& resourceList):
    QThread(parent),
    mLastModSW(newestSW),
    mLastModList(newestList),
    mResourceSW(resourceSW),
    mResourceList(resourceList),
    mSSLConnection(url, certificate)
{
}


Downloader::~Downloader() {
}

QString Downloader::getDataDirectory()
{
    QString candidate =
        QStandardPaths::writableLocation(QStandardPaths::DataLocation);

    if (candidate.isEmpty()) {
        qDebug() << "Could not find writeable locaction for me";
        return QString();
    }

    QDir cDir(candidate);

    if (!cDir.exists()) {
        if (!cDir.mkpath(candidate)) {
            qDebug() << "Could not create path to: " << candidate;
            return QString();
        }
    }
    return cDir.absolutePath();
}

QMap<QString, QString> Downloader::parseHeaders(QByteArray *data)
{
    int bodyStart = data->indexOf("\r\n\r\n");
    QMap<QString, QString> retval;
    QByteArray headers;
    QString response(*data);
    if (bodyStart == -1) {
        qDebug() << "Could not find header end.";
        emit error(tr("Invalid response"),
                SSLConnection::InvalidResponse);
        return retval;
    }

    /* Take the headers with one additional line break */
    headers = data->left(bodyStart + 2);
    /* Chop off the head */

    foreach (const QString& line, response.split("\r\n")) {
        int sepPos = -1;
        sepPos = line.indexOf(": ");
        if (sepPos == -1) {
            continue;
        }
        QString key = line.left(sepPos);
        QString value = line.right(line.size() - sepPos - 2);

        retval.insert(key, value);
    }

    *data = data->right(data->size() - bodyStart - 4);
    return retval;
}

QDateTime Downloader::getLastModifiedHeader(const QString &resource) {
    int ret = -1;
    QByteArray response;
    QLocale cLocale = QLocale::c();
    QMap<QString, QString> headers;
    QString headRequest =
        QString::fromLatin1("HEAD %1 HTTP/1.0\r\n\r\n").arg(resource);

    ret = mSSLConnection.write(headRequest.toUtf8());
    if (ret != 0) {
        emit error (tr("Connection lost"), SSLConnection::ConnectionLost);
        return QDateTime();
    }

    response = mSSLConnection.read(1024);

    if (response.isNull()) {
        qDebug() << "No response";
        emit error (tr("Connection lost"), SSLConnection::ConnectionLost);
        return QDateTime();
    }

    headers = parseHeaders(&response);
    const QString lastModified = headers.value("Last-Modified");
    qDebug() << "Headers: " << headers;
    if (!lastModified.isEmpty()) {
        QDateTime candidate = cLocale.toDateTime(lastModified,
                "ddd, dd MMM yyyy HH:mm:ss' GMT'");
        if (candidate.isValid()) {
            return candidate;
        }
    }
    emit error (tr("Invalid response from the server"), SSLConnection::InvalidResponse);
    qDebug() << "Response from server was: " << response;
    return QDateTime();
}

bool Downloader::downloadFile(const QString &resource,
                              const QString &fileName,
                              size_t maxSize)
{
    int ret = -1;
    size_t bytesRead = 0;
    QString getRequest =
        QString::fromLatin1("GET %1 HTTP/1.0\r\n\r\n").arg(resource);

    QSaveFile outputFile(fileName);

    ret = mSSLConnection.write(getRequest.toUtf8());

    // Open / Create the file to write to.
    if (!outputFile.open(QIODevice::WriteOnly)) {
        qDebug() << "Failed to open file";
        return false;
    }

    if (ret != 0) {
        emit error(tr("Connection lost"), SSLConnection::ConnectionLost);
        return false;
    }

    bool inBody = false;
    QMap <QString, QString> headers;
    do {
        /* Read the response in 8KiB chunks */
        int responseSize = 0;
        QByteArray response = mSSLConnection.read(8192);
        if (response.isNull()) {
            qDebug() << "Error reading response";
            emit error(tr("Connection lost"), SSLConnection::ConnectionLost);
            return false;
        }
        responseSize = response.size();
        if (!inBody) {
            headers = parseHeaders(&response);
            inBody = true;
        }
        outputFile.write(response);
        bytesRead += responseSize;
        if (responseSize < 8192) {
            /* Nothing more to read */
            break;
        }
        /* TODO Emit progress */
    } while (bytesRead < maxSize);

    return outputFile.commit();
}

void Downloader::run() {
    int ret;
    QDateTime remoteModList;
    QDateTime remoteModSW;

    if (!mSSLConnection.initialized()) {
        emit error(tr("Failed to initialize SSL Module."), SSLConnection::ErrUnknown);
        return;
    }

    ret = mSSLConnection.connect();

    if (ret != 0) {
        emit error(tr("Failed to connect."),
                   mSSLConnection.getLastError());
        return;
    }

    emit progress(tr("Connected"), 1, -1);

    remoteModSW = getLastModifiedHeader(mResourceSW);
    remoteModList = getLastModifiedHeader(mResourceList);

    if (!remoteModSW.isValid() || !remoteModList.isValid()) {
        qDebug() << "Could not parse headers";
        return;
    }

    if (!mLastModSW.isValid() || remoteModSW > mLastModSW) {
        QString dataDirectory = getDataDirectory();

        if (dataDirectory.isEmpty()) {
            qDebug() << "Failed to get data directory";
            return;
        }

        QString fileName = dataDirectory.append("/SW-")
            .append(remoteModSW.toString("yyyymmddHHmmss"))
            .append(".exe");

        qDebug() << "fileName: " << fileName;

        if (!downloadFile(mResourceSW, fileName, MAX_SW_SIZE)) {
            return;
        }

        emit newSoftwareAvailable(fileName, remoteModSW);
    } else if (!mLastModList.isValid() || remoteModList > mLastModList) {
        QString dataDirectory = getDataDirectory();

        if (dataDirectory.isEmpty()) {
            qDebug() << "Failed to get data directory";
            return;
        }

        QString fileName = dataDirectory.append("/list-")
            .append(remoteModSW.toString("yyyymmddHHmmss"))
            .append(".txt");

        qDebug() << "fileName: " << fileName;

        if (!downloadFile(mResourceList, fileName, MAX_LIST_SIZE)) {
            return;
        }

        emit newListAvailable(fileName, remoteModList);
    }

    emit progress(tr("Closing"), 1, -1);
}

http://wald.intevation.org/projects/trustbridge/