mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2024-10-01 01:06:10 -04:00
6b8e0f7ae4
Pre-release and post-release suffixes are now interpreted correctly. Also fix comparison of incomplete versions. Signed-off-by: Jared Van Bortel <jared@nomic.ai>
703 lines
25 KiB
C++
703 lines
25 KiB
C++
#include "download.h"
|
|
|
|
#include "modellist.h"
|
|
#include "mysettings.h"
|
|
#include "network.h"
|
|
|
|
#include <QByteArray>
|
|
#include <QCollator>
|
|
#include <QCoreApplication>
|
|
#include <QDebug>
|
|
#include <QGlobalStatic>
|
|
#include <QGuiApplication>
|
|
#include <QIODevice>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonValue>
|
|
#include <QLocale>
|
|
#include <QNetworkRequest>
|
|
#include <QPair>
|
|
#include <QSettings>
|
|
#include <QSslConfiguration>
|
|
#include <QSslSocket>
|
|
#include <QStringList>
|
|
#include <QTextStream>
|
|
#include <QUrl>
|
|
#include <QVariant>
|
|
#include <QVector>
|
|
#include <Qt>
|
|
#include <QtLogging>
|
|
|
|
#include <algorithm>
|
|
#include <compare>
|
|
#include <cstddef>
|
|
#include <utility>
|
|
|
|
using namespace Qt::Literals::StringLiterals;
|
|
|
|
class MyDownload: public Download { };
|
|
Q_GLOBAL_STATIC(MyDownload, downloadInstance)
|
|
Download *Download::globalInstance()
|
|
{
|
|
return downloadInstance();
|
|
}
|
|
|
|
Download::Download()
|
|
: QObject(nullptr)
|
|
, m_hashAndSave(new HashAndSaveFile)
|
|
{
|
|
connect(this, &Download::requestHashAndSave, m_hashAndSave,
|
|
&HashAndSaveFile::hashAndSave, Qt::QueuedConnection);
|
|
connect(m_hashAndSave, &HashAndSaveFile::hashAndSaveFinished, this,
|
|
&Download::handleHashAndSaveFinished, Qt::QueuedConnection);
|
|
connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this,
|
|
&Download::handleSslErrors);
|
|
updateLatestNews();
|
|
updateReleaseNotes();
|
|
m_startTime = QDateTime::currentDateTime();
|
|
}
|
|
|
|
static bool operator==(const ReleaseInfo& lhs, const ReleaseInfo& rhs)
|
|
{
|
|
return lhs.version == rhs.version;
|
|
}
|
|
|
|
std::strong_ordering Download::compareAppVersions(const QString &a, const QString &b)
|
|
{
|
|
static QRegularExpression versionRegex(R"(^(\d+(?:\.\d+){0,2})(-.+)?$)");
|
|
|
|
// When comparing versions, make sure a2 < a10.
|
|
QCollator versionCollator(QLocale(QLocale::English, QLocale::UnitedStates));
|
|
versionCollator.setNumericMode(true);
|
|
|
|
QRegularExpressionMatch aMatch = versionRegex.match(a);
|
|
QRegularExpressionMatch bMatch = versionRegex.match(b);
|
|
|
|
Q_ASSERT(aMatch.hasMatch() && bMatch.hasMatch()); // expect valid versions
|
|
|
|
// Check for an invalid version. foo < 3.0.0 -> !hasMatch < hasMatch
|
|
if (auto diff = aMatch.hasMatch() <=> bMatch.hasMatch(); diff != 0)
|
|
return diff; // invalid version compares as lower
|
|
|
|
// Compare invalid versions. fooa < foob
|
|
if (!aMatch.hasMatch() && !bMatch.hasMatch())
|
|
return versionCollator.compare(a, b) <=> 0; // lexicographic comparison
|
|
|
|
// Compare first three components. 3.0.0 < 3.0.1
|
|
QStringList aParts = aMatch.captured(1).split('.');
|
|
QStringList bParts = bMatch.captured(1).split('.');
|
|
for (int i = 0; i < qMax(aParts.size(), bParts.size()); i++) {
|
|
bool ok = false;
|
|
int aInt = aParts.value(i, "0").toInt(&ok);
|
|
Q_ASSERT(ok);
|
|
int bInt = bParts.value(i, "0").toInt(&ok);
|
|
Q_ASSERT(ok);
|
|
if (auto diff = aInt <=> bInt; diff != 0)
|
|
return diff; // version with lower component compares as lower
|
|
}
|
|
|
|
// Check for a pre/post-release suffix. 3.0.0-dev0 < 3.0.0-rc1 < 3.0.0 < 3.0.0-post1
|
|
auto getSuffixOrder = [](const QRegularExpressionMatch &match) -> int {
|
|
QString suffix = match.captured(2);
|
|
return suffix.startsWith("-dev") ? 0 :
|
|
suffix.startsWith("-rc") ? 1 :
|
|
suffix.isEmpty() ? 2 :
|
|
/* some other suffix */ 3;
|
|
};
|
|
if (auto diff = getSuffixOrder(aMatch) <=> getSuffixOrder(bMatch); diff != 0)
|
|
return diff; // different suffix types
|
|
|
|
// Lexicographic comparison of suffix. 3.0.0-rc1 < 3.0.0-rc2
|
|
if (aMatch.hasCaptured(2) && bMatch.hasCaptured(2)) {
|
|
if (auto diff = versionCollator.compare(aMatch.captured(2), bMatch.captured(2)); diff != 0)
|
|
return diff <=> 0;
|
|
}
|
|
|
|
return std::strong_ordering::equal;
|
|
}
|
|
|
|
ReleaseInfo Download::releaseInfo() const
|
|
{
|
|
const QString currentVersion = QCoreApplication::applicationVersion();
|
|
if (m_releaseMap.contains(currentVersion))
|
|
return m_releaseMap.value(currentVersion);
|
|
if (!m_releaseMap.empty())
|
|
return m_releaseMap.last();
|
|
return ReleaseInfo();
|
|
}
|
|
|
|
bool Download::hasNewerRelease() const
|
|
{
|
|
const QString currentVersion = QCoreApplication::applicationVersion();
|
|
for (const auto &version : m_releaseMap.keys()) {
|
|
if (compareAppVersions(version, currentVersion) > 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Download::isFirstStart(bool writeVersion) const
|
|
{
|
|
auto *mySettings = MySettings::globalInstance();
|
|
|
|
QSettings settings;
|
|
QString lastVersionStarted = settings.value("download/lastVersionStarted").toString();
|
|
bool first = lastVersionStarted != QCoreApplication::applicationVersion();
|
|
if (first && writeVersion) {
|
|
settings.setValue("download/lastVersionStarted", QCoreApplication::applicationVersion());
|
|
// let the user select these again
|
|
settings.remove("network/usageStatsActive");
|
|
settings.remove("network/isActive");
|
|
emit mySettings->networkUsageStatsActiveChanged();
|
|
emit mySettings->networkIsActiveChanged();
|
|
}
|
|
|
|
return first || !mySettings->isNetworkUsageStatsActiveSet() || !mySettings->isNetworkIsActiveSet();
|
|
}
|
|
|
|
void Download::updateReleaseNotes()
|
|
{
|
|
QUrl jsonUrl("http://gpt4all.io/meta/release.json");
|
|
QNetworkRequest request(jsonUrl);
|
|
QSslConfiguration conf = request.sslConfiguration();
|
|
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
request.setSslConfiguration(conf);
|
|
QNetworkReply *jsonReply = m_networkManager.get(request);
|
|
connect(qGuiApp, &QCoreApplication::aboutToQuit, jsonReply, &QNetworkReply::abort);
|
|
connect(jsonReply, &QNetworkReply::finished, this, &Download::handleReleaseJsonDownloadFinished);
|
|
}
|
|
|
|
void Download::updateLatestNews()
|
|
{
|
|
QUrl url("http://gpt4all.io/meta/latestnews.md");
|
|
QNetworkRequest request(url);
|
|
QSslConfiguration conf = request.sslConfiguration();
|
|
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
request.setSslConfiguration(conf);
|
|
QNetworkReply *reply = m_networkManager.get(request);
|
|
connect(qGuiApp, &QCoreApplication::aboutToQuit, reply, &QNetworkReply::abort);
|
|
connect(reply, &QNetworkReply::finished, this, &Download::handleLatestNewsDownloadFinished);
|
|
}
|
|
|
|
void Download::downloadModel(const QString &modelFile)
|
|
{
|
|
QFile *tempFile = new QFile(ModelList::globalInstance()->incompleteDownloadPath(modelFile));
|
|
bool success = tempFile->open(QIODevice::WriteOnly | QIODevice::Append);
|
|
qWarning() << "Opening temp file for writing:" << tempFile->fileName();
|
|
if (!success) {
|
|
const QString error
|
|
= u"ERROR: Could not open temp file: %1 %2"_s.arg(tempFile->fileName(), modelFile);
|
|
qWarning() << error;
|
|
clearRetry(modelFile);
|
|
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::DownloadErrorRole, error }});
|
|
return;
|
|
}
|
|
tempFile->flush();
|
|
size_t incomplete_size = tempFile->size();
|
|
if (incomplete_size > 0) {
|
|
bool success = tempFile->seek(incomplete_size);
|
|
if (!success) {
|
|
incomplete_size = 0;
|
|
success = tempFile->seek(incomplete_size);
|
|
Q_ASSERT(success);
|
|
}
|
|
}
|
|
|
|
if (!ModelList::globalInstance()->containsByFilename(modelFile)) {
|
|
qWarning() << "ERROR: Could not find file:" << modelFile;
|
|
return;
|
|
}
|
|
|
|
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::DownloadingRole, true }});
|
|
ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
|
|
QString url = !info.url().isEmpty() ? info.url() : "http://gpt4all.io/models/gguf/" + modelFile;
|
|
Network::globalInstance()->trackEvent("download_started", { {"model", modelFile} });
|
|
QNetworkRequest request(url);
|
|
request.setAttribute(QNetworkRequest::User, modelFile);
|
|
request.setRawHeader("range", u"bytes=%1-"_s.arg(tempFile->pos()).toUtf8());
|
|
QSslConfiguration conf = request.sslConfiguration();
|
|
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
|
|
request.setSslConfiguration(conf);
|
|
QNetworkReply *modelReply = m_networkManager.get(request);
|
|
connect(qGuiApp, &QCoreApplication::aboutToQuit, modelReply, &QNetworkReply::abort);
|
|
connect(modelReply, &QNetworkReply::downloadProgress, this, &Download::handleDownloadProgress);
|
|
connect(modelReply, &QNetworkReply::errorOccurred, this, &Download::handleErrorOccurred);
|
|
connect(modelReply, &QNetworkReply::finished, this, &Download::handleModelDownloadFinished);
|
|
connect(modelReply, &QNetworkReply::readyRead, this, &Download::handleReadyRead);
|
|
m_activeDownloads.insert(modelReply, tempFile);
|
|
}
|
|
|
|
void Download::cancelDownload(const QString &modelFile)
|
|
{
|
|
for (auto [modelReply, tempFile]: m_activeDownloads.asKeyValueRange()) {
|
|
QUrl url = modelReply->request().url();
|
|
if (url.toString().endsWith(modelFile)) {
|
|
Network::globalInstance()->trackEvent("download_canceled", { {"model", modelFile} });
|
|
|
|
// Disconnect the signals
|
|
disconnect(modelReply, &QNetworkReply::downloadProgress, this, &Download::handleDownloadProgress);
|
|
disconnect(modelReply, &QNetworkReply::finished, this, &Download::handleModelDownloadFinished);
|
|
|
|
modelReply->abort(); // Abort the download
|
|
modelReply->deleteLater(); // Schedule the reply for deletion
|
|
|
|
tempFile->deleteLater();
|
|
m_activeDownloads.remove(modelReply);
|
|
|
|
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::DownloadingRole, false }});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Download::installModel(const QString &modelFile, const QString &apiKey)
|
|
{
|
|
Q_ASSERT(!apiKey.isEmpty());
|
|
if (apiKey.isEmpty())
|
|
return;
|
|
|
|
Network::globalInstance()->trackEvent("install_model", { {"model", modelFile} });
|
|
|
|
QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
|
|
QFile file(filePath);
|
|
if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) {
|
|
|
|
QJsonObject obj;
|
|
QString modelName(modelFile);
|
|
modelName.remove(0, 8); // strip "gpt4all-" prefix
|
|
modelName.chop(7); // strip ".rmodel" extension
|
|
obj.insert("apiKey", apiKey);
|
|
obj.insert("modelName", modelName);
|
|
QJsonDocument doc(obj);
|
|
|
|
QTextStream stream(&file);
|
|
stream << doc.toJson();
|
|
file.close();
|
|
ModelList::globalInstance()->updateModelsFromDirectory();
|
|
emit toastMessage(tr("Model \"%1\" is installed successfully.").arg(modelName));
|
|
}
|
|
|
|
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::InstalledRole, true }});
|
|
}
|
|
|
|
void Download::installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl)
|
|
{
|
|
Q_ASSERT(!modelName.isEmpty());
|
|
if (modelName.isEmpty()) {
|
|
emit toastMessage(tr("ERROR: $MODEL_NAME is empty."));
|
|
return;
|
|
}
|
|
|
|
Q_ASSERT(!apiKey.isEmpty());
|
|
if (apiKey.isEmpty()) {
|
|
emit toastMessage(tr("ERROR: $API_KEY is empty."));
|
|
return;
|
|
}
|
|
|
|
QUrl apiBaseUrl(QUrl::fromUserInput(baseUrl));
|
|
if (!Network::isHttpUrlValid(baseUrl)) {
|
|
emit toastMessage(tr("ERROR: $BASE_URL is invalid."));
|
|
return;
|
|
}
|
|
|
|
QString modelFile(ModelList::compatibleModelFilename(baseUrl, modelName));
|
|
if (ModelList::globalInstance()->contains(modelFile)) {
|
|
emit toastMessage(tr("ERROR: Model \"%1 (%2)\" is conflict.").arg(modelName, baseUrl));
|
|
return;
|
|
}
|
|
ModelList::globalInstance()->addModel(modelFile);
|
|
Network::globalInstance()->trackEvent("install_model", { {"model", modelFile} });
|
|
|
|
QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
|
|
QFile file(filePath);
|
|
if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) {
|
|
QJsonObject obj;
|
|
obj.insert("apiKey", apiKey);
|
|
obj.insert("modelName", modelName);
|
|
obj.insert("baseUrl", apiBaseUrl.toString());
|
|
QJsonDocument doc(obj);
|
|
|
|
QTextStream stream(&file);
|
|
stream << doc.toJson();
|
|
file.close();
|
|
ModelList::globalInstance()->updateModelsFromDirectory();
|
|
emit toastMessage(tr("Model \"%1 (%2)\" is installed successfully.").arg(modelName, baseUrl));
|
|
}
|
|
|
|
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::InstalledRole, true }});
|
|
}
|
|
|
|
void Download::removeModel(const QString &modelFile)
|
|
{
|
|
const QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
|
|
QFile incompleteFile(ModelList::globalInstance()->incompleteDownloadPath(modelFile));
|
|
if (incompleteFile.exists()) {
|
|
incompleteFile.remove();
|
|
}
|
|
|
|
bool shouldRemoveInstalled = false;
|
|
QFile file(filePath);
|
|
if (file.exists()) {
|
|
const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
|
|
MySettings::globalInstance()->eraseModel(info);
|
|
shouldRemoveInstalled = info.installed && !info.isClone() && (info.isDiscovered() || info.isCompatibleApi || info.description() == "" /*indicates sideloaded*/);
|
|
if (shouldRemoveInstalled)
|
|
ModelList::globalInstance()->removeInstalled(info);
|
|
Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
|
|
file.remove();
|
|
emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name()));
|
|
}
|
|
|
|
if (!shouldRemoveInstalled) {
|
|
QVector<QPair<int, QVariant>> data {
|
|
{ ModelList::InstalledRole, false },
|
|
{ ModelList::BytesReceivedRole, 0 },
|
|
{ ModelList::BytesTotalRole, 0 },
|
|
{ ModelList::TimestampRole, 0 },
|
|
{ ModelList::SpeedRole, QString() },
|
|
{ ModelList::DownloadErrorRole, QString() },
|
|
};
|
|
ModelList::globalInstance()->updateDataByFilename(modelFile, data);
|
|
}
|
|
}
|
|
|
|
void Download::handleSslErrors(QNetworkReply *reply, const QList<QSslError> &errors)
|
|
{
|
|
QUrl url = reply->request().url();
|
|
for (const auto &e : errors)
|
|
qWarning() << "ERROR: Received ssl error:" << e.errorString() << "for" << url;
|
|
}
|
|
|
|
void Download::handleReleaseJsonDownloadFinished()
|
|
{
|
|
QNetworkReply *jsonReply = qobject_cast<QNetworkReply *>(sender());
|
|
if (!jsonReply)
|
|
return;
|
|
|
|
QByteArray jsonData = jsonReply->readAll();
|
|
jsonReply->deleteLater();
|
|
parseReleaseJsonFile(jsonData);
|
|
}
|
|
|
|
void Download::parseReleaseJsonFile(const QByteArray &jsonData)
|
|
{
|
|
QJsonParseError err;
|
|
QJsonDocument document = QJsonDocument::fromJson(jsonData, &err);
|
|
if (err.error != QJsonParseError::NoError) {
|
|
qWarning() << "ERROR: Couldn't parse: " << jsonData << err.errorString();
|
|
return;
|
|
}
|
|
|
|
QJsonArray jsonArray = document.array();
|
|
|
|
m_releaseMap.clear();
|
|
for (const QJsonValue &value : jsonArray) {
|
|
QJsonObject obj = value.toObject();
|
|
|
|
QString version = obj["version"].toString();
|
|
QString notes = obj["notes"].toString();
|
|
QString contributors = obj["contributors"].toString();
|
|
ReleaseInfo releaseInfo;
|
|
releaseInfo.version = version;
|
|
releaseInfo.notes = notes;
|
|
releaseInfo.contributors = contributors;
|
|
m_releaseMap.insert(version, releaseInfo);
|
|
}
|
|
|
|
emit hasNewerReleaseChanged();
|
|
emit releaseInfoChanged();
|
|
}
|
|
|
|
void Download::handleLatestNewsDownloadFinished()
|
|
{
|
|
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
|
if (!reply)
|
|
return;
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
qWarning() << "ERROR: network error occurred attempting to download latest news:" << reply->errorString();
|
|
reply->deleteLater();
|
|
return;
|
|
}
|
|
|
|
QByteArray responseData = reply->readAll();
|
|
m_latestNews = QString::fromUtf8(responseData);
|
|
reply->deleteLater();
|
|
emit latestNewsChanged();
|
|
}
|
|
|
|
bool Download::hasRetry(const QString &filename) const
|
|
{
|
|
return m_activeRetries.contains(filename);
|
|
}
|
|
|
|
bool Download::shouldRetry(const QString &filename)
|
|
{
|
|
int retries = 0;
|
|
if (m_activeRetries.contains(filename))
|
|
retries = m_activeRetries.value(filename);
|
|
|
|
++retries;
|
|
|
|
// Allow up to ten retries for now
|
|
if (retries < 10) {
|
|
m_activeRetries.insert(filename, retries);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Download::clearRetry(const QString &filename)
|
|
{
|
|
m_activeRetries.remove(filename);
|
|
}
|
|
|
|
void Download::handleErrorOccurred(QNetworkReply::NetworkError code)
|
|
{
|
|
QNetworkReply *modelReply = qobject_cast<QNetworkReply *>(sender());
|
|
if (!modelReply)
|
|
return;
|
|
|
|
// This occurs when the user explicitly cancels the download
|
|
if (code == QNetworkReply::OperationCanceledError)
|
|
return;
|
|
|
|
QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString();
|
|
if (shouldRetry(modelFilename)) {
|
|
downloadModel(modelFilename);
|
|
return;
|
|
}
|
|
|
|
clearRetry(modelFilename);
|
|
|
|
const QString error
|
|
= u"ERROR: Network error occurred attempting to download %1 code: %2 errorString %3"_s
|
|
.arg(modelFilename)
|
|
.arg(code)
|
|
.arg(modelReply->errorString());
|
|
qWarning() << error;
|
|
ModelList::globalInstance()->updateDataByFilename(modelFilename, {{ ModelList::DownloadErrorRole, error }});
|
|
Network::globalInstance()->trackEvent("download_error", {
|
|
{"model", modelFilename},
|
|
{"code", (int)code},
|
|
{"error", modelReply->errorString()},
|
|
});
|
|
cancelDownload(modelFilename);
|
|
}
|
|
|
|
void Download::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
|
{
|
|
QNetworkReply *modelReply = qobject_cast<QNetworkReply *>(sender());
|
|
if (!modelReply)
|
|
return;
|
|
QFile *tempFile = m_activeDownloads.value(modelReply);
|
|
if (!tempFile)
|
|
return;
|
|
QString contentRange = modelReply->rawHeader("content-range");
|
|
if (contentRange.contains("/")) {
|
|
QString contentTotalSize = contentRange.split("/").last();
|
|
bytesTotal = contentTotalSize.toLongLong();
|
|
}
|
|
|
|
const QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString();
|
|
const qint64 lastUpdate = ModelList::globalInstance()->dataByFilename(modelFilename, ModelList::TimestampRole).toLongLong();
|
|
const qint64 currentUpdate = QDateTime::currentMSecsSinceEpoch();
|
|
if (currentUpdate - lastUpdate < 1000)
|
|
return;
|
|
|
|
const qint64 lastBytesReceived = ModelList::globalInstance()->dataByFilename(modelFilename, ModelList::BytesReceivedRole).toLongLong();
|
|
const qint64 currentBytesReceived = tempFile->pos();
|
|
|
|
qint64 timeDifference = currentUpdate - lastUpdate;
|
|
qint64 bytesDifference = currentBytesReceived - lastBytesReceived;
|
|
qint64 speed = (bytesDifference / timeDifference) * 1000; // bytes per second
|
|
QString speedText;
|
|
if (speed < 1024)
|
|
speedText = QString::number(static_cast<double>(speed), 'f', 2) + " B/s";
|
|
else if (speed < 1024 * 1024)
|
|
speedText = QString::number(static_cast<double>(speed / 1024.0), 'f', 2) + " KB/s";
|
|
else
|
|
speedText = QString::number(static_cast<double>(speed / (1024.0 * 1024.0)), 'f', 2) + " MB/s";
|
|
|
|
QVector<QPair<int, QVariant>> data {
|
|
{ ModelList::BytesReceivedRole, currentBytesReceived },
|
|
{ ModelList::BytesTotalRole, bytesTotal },
|
|
{ ModelList::SpeedRole, speedText },
|
|
{ ModelList::TimestampRole, currentUpdate },
|
|
};
|
|
ModelList::globalInstance()->updateDataByFilename(modelFilename, data);
|
|
}
|
|
|
|
HashAndSaveFile::HashAndSaveFile()
|
|
: QObject(nullptr)
|
|
{
|
|
moveToThread(&m_hashAndSaveThread);
|
|
m_hashAndSaveThread.setObjectName("hashandsave thread");
|
|
m_hashAndSaveThread.start();
|
|
}
|
|
|
|
void HashAndSaveFile::hashAndSave(const QString &expectedHash, QCryptographicHash::Algorithm a,
|
|
const QString &saveFilePath, QFile *tempFile, QNetworkReply *modelReply)
|
|
{
|
|
Q_ASSERT(!tempFile->isOpen());
|
|
QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString();
|
|
|
|
// Reopen the tempFile for hashing
|
|
if (!tempFile->open(QIODevice::ReadOnly)) {
|
|
const QString error
|
|
= u"ERROR: Could not open temp file for hashing: %1 %2"_s.arg(tempFile->fileName(), modelFilename);
|
|
qWarning() << error;
|
|
emit hashAndSaveFinished(false, error, tempFile, modelReply);
|
|
return;
|
|
}
|
|
|
|
QCryptographicHash hash(a);
|
|
while(!tempFile->atEnd())
|
|
hash.addData(tempFile->read(16384));
|
|
if (hash.result().toHex() != expectedHash.toLatin1()) {
|
|
tempFile->close();
|
|
const QString error
|
|
= u"ERROR: Download error hash did not match: %1 != %2 for %3"_s
|
|
.arg(hash.result().toHex(), expectedHash.toLatin1(), modelFilename);
|
|
qWarning() << error;
|
|
tempFile->remove();
|
|
emit hashAndSaveFinished(false, error, tempFile, modelReply);
|
|
return;
|
|
}
|
|
|
|
// The file save needs the tempFile closed
|
|
tempFile->close();
|
|
|
|
// Attempt to *move* the verified tempfile into place - this should be atomic
|
|
// but will only work if the destination is on the same filesystem
|
|
if (tempFile->rename(saveFilePath)) {
|
|
emit hashAndSaveFinished(true, QString(), tempFile, modelReply);
|
|
ModelList::globalInstance()->updateModelsFromDirectory();
|
|
return;
|
|
}
|
|
|
|
// Reopen the tempFile for copying
|
|
if (!tempFile->open(QIODevice::ReadOnly)) {
|
|
const QString error
|
|
= u"ERROR: Could not open temp file at finish: %1 %2"_s.arg(tempFile->fileName(), modelFilename);
|
|
qWarning() << error;
|
|
emit hashAndSaveFinished(false, error, tempFile, modelReply);
|
|
return;
|
|
}
|
|
|
|
// Save the model file to disk
|
|
QFile file(saveFilePath);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
QByteArray buffer;
|
|
while (!tempFile->atEnd()) {
|
|
buffer = tempFile->read(16384);
|
|
file.write(buffer);
|
|
}
|
|
file.close();
|
|
tempFile->close();
|
|
emit hashAndSaveFinished(true, QString(), tempFile, modelReply);
|
|
} else {
|
|
QFile::FileError error = file.error();
|
|
const QString errorString
|
|
= u"ERROR: Could not save model to location: %1 failed with code %1"_s.arg(saveFilePath).arg(error);
|
|
qWarning() << errorString;
|
|
tempFile->close();
|
|
emit hashAndSaveFinished(false, errorString, tempFile, modelReply);
|
|
}
|
|
|
|
ModelList::globalInstance()->updateModelsFromDirectory();
|
|
}
|
|
|
|
void Download::handleModelDownloadFinished()
|
|
{
|
|
QNetworkReply *modelReply = qobject_cast<QNetworkReply *>(sender());
|
|
if (!modelReply)
|
|
return;
|
|
|
|
QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString();
|
|
QFile *tempFile = m_activeDownloads.value(modelReply);
|
|
m_activeDownloads.remove(modelReply);
|
|
|
|
if (modelReply->error()) {
|
|
const QString errorString
|
|
= u"ERROR: Downloading failed with code %1 \"%2\""_s.arg(modelReply->error()).arg(modelReply->errorString());
|
|
qWarning() << errorString;
|
|
modelReply->deleteLater();
|
|
tempFile->deleteLater();
|
|
if (!hasRetry(modelFilename)) {
|
|
QVector<QPair<int, QVariant>> data {
|
|
{ ModelList::DownloadingRole, false },
|
|
{ ModelList::DownloadErrorRole, errorString },
|
|
};
|
|
ModelList::globalInstance()->updateDataByFilename(modelFilename, data);
|
|
}
|
|
return;
|
|
}
|
|
|
|
clearRetry(modelFilename);
|
|
|
|
// The hash and save needs the tempFile closed
|
|
tempFile->close();
|
|
|
|
if (!ModelList::globalInstance()->containsByFilename(modelFilename)) {
|
|
qWarning() << "ERROR: downloading no such file:" << modelFilename;
|
|
modelReply->deleteLater();
|
|
tempFile->deleteLater();
|
|
return;
|
|
}
|
|
|
|
// Notify that we are calculating hash
|
|
ModelList::globalInstance()->updateDataByFilename(modelFilename, {{ ModelList::CalcHashRole, true }});
|
|
QByteArray hash = ModelList::globalInstance()->modelInfoByFilename(modelFilename).hash;
|
|
ModelInfo::HashAlgorithm hashAlgorithm = ModelList::globalInstance()->modelInfoByFilename(modelFilename).hashAlgorithm;
|
|
const QString saveFilePath = MySettings::globalInstance()->modelPath() + modelFilename;
|
|
emit requestHashAndSave(hash,
|
|
(hashAlgorithm == ModelInfo::Md5 ? QCryptographicHash::Md5 : QCryptographicHash::Sha256),
|
|
saveFilePath, tempFile, modelReply);
|
|
}
|
|
|
|
void Download::handleHashAndSaveFinished(bool success, const QString &error,
|
|
QFile *tempFile, QNetworkReply *modelReply)
|
|
{
|
|
// The hash and save should send back with tempfile closed
|
|
Q_ASSERT(!tempFile->isOpen());
|
|
QString modelFilename = modelReply->request().attribute(QNetworkRequest::User).toString();
|
|
Network::globalInstance()->trackEvent("download_finished", { {"model", modelFilename}, {"success", success} });
|
|
|
|
QVector<QPair<int, QVariant>> data {
|
|
{ ModelList::CalcHashRole, false },
|
|
{ ModelList::DownloadingRole, false },
|
|
};
|
|
|
|
modelReply->deleteLater();
|
|
tempFile->deleteLater();
|
|
|
|
if (!success) {
|
|
data.append({ ModelList::DownloadErrorRole, error });
|
|
} else {
|
|
data.append({ ModelList::DownloadErrorRole, QString() });
|
|
ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFilename);
|
|
if (info.isDiscovered())
|
|
ModelList::globalInstance()->updateDiscoveredInstalled(info);
|
|
}
|
|
|
|
ModelList::globalInstance()->updateDataByFilename(modelFilename, data);
|
|
}
|
|
|
|
void Download::handleReadyRead()
|
|
{
|
|
QNetworkReply *modelReply = qobject_cast<QNetworkReply *>(sender());
|
|
if (!modelReply)
|
|
return;
|
|
|
|
QFile *tempFile = m_activeDownloads.value(modelReply);
|
|
QByteArray buffer;
|
|
while (!modelReply->atEnd()) {
|
|
buffer = modelReply->read(16384);
|
|
tempFile->write(buffer);
|
|
}
|
|
tempFile->flush();
|
|
}
|