mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2025-08-11 12:52:06 +00:00
download: make model downloads resumable
* save files as `incomplete-{filename}` in the dest folder * rename into place after hash is confirmed or delete if hash is bad * resume downloads using http `range` * if DL is resumed from a different app session rewind a bit - this is to deal with the case where the file size changes before the content is fully flushed out * flush dest file at end of readyRead, this mitigates the above and provides backpressure on the download if the destination disk is slower than the network connection
This commit is contained in:
parent
4a09f0f0ec
commit
edad3baa99
52
download.cpp
52
download.cpp
@ -36,6 +36,7 @@ Download::Download()
|
|||||||
settings.sync();
|
settings.sync();
|
||||||
m_downloadLocalModelsPath = settings.value("modelPath",
|
m_downloadLocalModelsPath = settings.value("modelPath",
|
||||||
defaultLocalModelsPath()).toString();
|
defaultLocalModelsPath()).toString();
|
||||||
|
m_startTime = QDateTime::currentDateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool operator==(const ModelInfo& lhs, const ModelInfo& rhs) {
|
bool operator==(const ModelInfo& lhs, const ModelInfo& rhs) {
|
||||||
@ -143,6 +144,12 @@ bool Download::isFirstStart() const
|
|||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Download::incompleteDownloadPath(const QString &modelFile) {
|
||||||
|
QString downloadPath = downloadLocalModelsPath() + "incomplete-" +
|
||||||
|
modelFile;
|
||||||
|
return downloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
QString Download::defaultLocalModelsPath() const
|
QString Download::defaultLocalModelsPath() const
|
||||||
{
|
{
|
||||||
QString localPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
|
QString localPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)
|
||||||
@ -194,17 +201,31 @@ void Download::updateReleaseNotes()
|
|||||||
|
|
||||||
void Download::downloadModel(const QString &modelFile)
|
void Download::downloadModel(const QString &modelFile)
|
||||||
{
|
{
|
||||||
QTemporaryFile *tempFile = new QTemporaryFile;
|
QFile *tempFile = new QFile(incompleteDownloadPath(modelFile));
|
||||||
bool success = tempFile->open();
|
QDateTime modTime = tempFile->fileTime(QFile::FileModificationTime);
|
||||||
|
bool success = tempFile->open(QIODevice::WriteOnly | QIODevice::Append);
|
||||||
qWarning() << "Opening temp file for writing:" << tempFile->fileName();
|
qWarning() << "Opening temp file for writing:" << tempFile->fileName();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
qWarning() << "ERROR: Could not open temp file:"
|
qWarning() << "ERROR: Could not open temp file:"
|
||||||
<< tempFile->fileName() << modelFile;
|
<< tempFile->fileName() << modelFile;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
size_t incomplete_size = tempFile->size();
|
||||||
|
if (incomplete_size > 0) {
|
||||||
|
if (modTime < m_startTime) {
|
||||||
|
qWarning() << "File last modified before app started, rewinding by 1MB";
|
||||||
|
if (incomplete_size >= 1024 * 1024) {
|
||||||
|
incomplete_size -= 1024 * 1024;
|
||||||
|
} else {
|
||||||
|
incomplete_size = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempFile->seek(incomplete_size);
|
||||||
|
}
|
||||||
|
|
||||||
Network::globalInstance()->sendDownloadStarted(modelFile);
|
Network::globalInstance()->sendDownloadStarted(modelFile);
|
||||||
QNetworkRequest request("http://gpt4all.io/models/" + modelFile);
|
QNetworkRequest request("http://gpt4all.io/models/" + modelFile);
|
||||||
|
request.setRawHeader("range", QString("bytes=%1-").arg(incomplete_size).toUtf8());
|
||||||
QSslConfiguration conf = request.sslConfiguration();
|
QSslConfiguration conf = request.sslConfiguration();
|
||||||
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
|
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||||
request.setSslConfiguration(conf);
|
request.setSslConfiguration(conf);
|
||||||
@ -230,7 +251,7 @@ void Download::cancelDownload(const QString &modelFile)
|
|||||||
modelReply->abort(); // Abort the download
|
modelReply->abort(); // Abort the download
|
||||||
modelReply->deleteLater(); // Schedule the reply for deletion
|
modelReply->deleteLater(); // Schedule the reply for deletion
|
||||||
|
|
||||||
QTemporaryFile *tempFile = m_activeDownloads.value(modelReply);
|
QFile *tempFile = m_activeDownloads.value(modelReply);
|
||||||
tempFile->deleteLater();
|
tempFile->deleteLater();
|
||||||
m_activeDownloads.remove(modelReply);
|
m_activeDownloads.remove(modelReply);
|
||||||
|
|
||||||
@ -410,9 +431,17 @@ void Download::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
|||||||
QNetworkReply *modelReply = qobject_cast<QNetworkReply *>(sender());
|
QNetworkReply *modelReply = qobject_cast<QNetworkReply *>(sender());
|
||||||
if (!modelReply)
|
if (!modelReply)
|
||||||
return;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
QString modelFilename = modelReply->url().fileName();
|
QString modelFilename = modelReply->url().fileName();
|
||||||
emit downloadProgress(bytesReceived, bytesTotal, modelFilename);
|
emit downloadProgress(tempFile->pos(), bytesTotal, modelFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
HashAndSaveFile::HashAndSaveFile()
|
HashAndSaveFile::HashAndSaveFile()
|
||||||
@ -424,13 +453,13 @@ HashAndSaveFile::HashAndSaveFile()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HashAndSaveFile::hashAndSave(const QString &expectedHash, const QString &saveFilePath,
|
void HashAndSaveFile::hashAndSave(const QString &expectedHash, const QString &saveFilePath,
|
||||||
QTemporaryFile *tempFile, QNetworkReply *modelReply)
|
QFile *tempFile, QNetworkReply *modelReply)
|
||||||
{
|
{
|
||||||
Q_ASSERT(!tempFile->isOpen());
|
Q_ASSERT(!tempFile->isOpen());
|
||||||
QString modelFilename = modelReply->url().fileName();
|
QString modelFilename = modelReply->url().fileName();
|
||||||
|
|
||||||
// Reopen the tempFile for hashing
|
// Reopen the tempFile for hashing
|
||||||
if (!tempFile->open()) {
|
if (!tempFile->open(QIODevice::ReadOnly)) {
|
||||||
qWarning() << "ERROR: Could not open temp file for hashing:"
|
qWarning() << "ERROR: Could not open temp file for hashing:"
|
||||||
<< tempFile->fileName() << modelFilename;
|
<< tempFile->fileName() << modelFilename;
|
||||||
emit hashAndSaveFinished(false, tempFile, modelReply);
|
emit hashAndSaveFinished(false, tempFile, modelReply);
|
||||||
@ -445,6 +474,7 @@ void HashAndSaveFile::hashAndSave(const QString &expectedHash, const QString &sa
|
|||||||
qWarning() << "ERROR: Download error MD5SUM did not match:"
|
qWarning() << "ERROR: Download error MD5SUM did not match:"
|
||||||
<< hash.result().toHex()
|
<< hash.result().toHex()
|
||||||
<< "!=" << expectedHash << "for" << modelFilename;
|
<< "!=" << expectedHash << "for" << modelFilename;
|
||||||
|
tempFile->remove();
|
||||||
emit hashAndSaveFinished(false, tempFile, modelReply);
|
emit hashAndSaveFinished(false, tempFile, modelReply);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -455,13 +485,12 @@ void HashAndSaveFile::hashAndSave(const QString &expectedHash, const QString &sa
|
|||||||
// Attempt to *move* the verified tempfile into place - this should be atomic
|
// Attempt to *move* the verified tempfile into place - this should be atomic
|
||||||
// but will only work if the destination is on the same filesystem
|
// but will only work if the destination is on the same filesystem
|
||||||
if (tempFile->rename(saveFilePath)) {
|
if (tempFile->rename(saveFilePath)) {
|
||||||
tempFile->setAutoRemove(false);
|
|
||||||
emit hashAndSaveFinished(true, tempFile, modelReply);
|
emit hashAndSaveFinished(true, tempFile, modelReply);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reopen the tempFile for copying
|
// Reopen the tempFile for copying
|
||||||
if (!tempFile->open()) {
|
if (!tempFile->open(QIODevice::ReadOnly)) {
|
||||||
qWarning() << "ERROR: Could not open temp file at finish:"
|
qWarning() << "ERROR: Could not open temp file at finish:"
|
||||||
<< tempFile->fileName() << modelFilename;
|
<< tempFile->fileName() << modelFilename;
|
||||||
emit hashAndSaveFinished(false, tempFile, modelReply);
|
emit hashAndSaveFinished(false, tempFile, modelReply);
|
||||||
@ -497,7 +526,7 @@ void Download::handleModelDownloadFinished()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
QString modelFilename = modelReply->url().fileName();
|
QString modelFilename = modelReply->url().fileName();
|
||||||
QTemporaryFile *tempFile = m_activeDownloads.value(modelReply);
|
QFile *tempFile = m_activeDownloads.value(modelReply);
|
||||||
m_activeDownloads.remove(modelReply);
|
m_activeDownloads.remove(modelReply);
|
||||||
|
|
||||||
if (modelReply->error()) {
|
if (modelReply->error()) {
|
||||||
@ -522,7 +551,7 @@ void Download::handleModelDownloadFinished()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Download::handleHashAndSaveFinished(bool success,
|
void Download::handleHashAndSaveFinished(bool success,
|
||||||
QTemporaryFile *tempFile, QNetworkReply *modelReply)
|
QFile *tempFile, QNetworkReply *modelReply)
|
||||||
{
|
{
|
||||||
// The hash and save should send back with tempfile closed
|
// The hash and save should send back with tempfile closed
|
||||||
Q_ASSERT(!tempFile->isOpen());
|
Q_ASSERT(!tempFile->isOpen());
|
||||||
@ -547,10 +576,11 @@ void Download::handleReadyRead()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
QString modelFilename = modelReply->url().fileName();
|
QString modelFilename = modelReply->url().fileName();
|
||||||
QTemporaryFile *tempFile = m_activeDownloads.value(modelReply);
|
QFile *tempFile = m_activeDownloads.value(modelReply);
|
||||||
QByteArray buffer;
|
QByteArray buffer;
|
||||||
while (!modelReply->atEnd()) {
|
while (!modelReply->atEnd()) {
|
||||||
buffer = modelReply->read(16384);
|
buffer = modelReply->read(16384);
|
||||||
tempFile->write(buffer);
|
tempFile->write(buffer);
|
||||||
}
|
}
|
||||||
|
tempFile->flush();
|
||||||
}
|
}
|
||||||
|
12
download.h
12
download.h
@ -56,11 +56,11 @@ public:
|
|||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void hashAndSave(const QString &hash, const QString &saveFilePath,
|
void hashAndSave(const QString &hash, const QString &saveFilePath,
|
||||||
QTemporaryFile *tempFile, QNetworkReply *modelReply);
|
QFile *tempFile, QNetworkReply *modelReply);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void hashAndSaveFinished(bool success,
|
void hashAndSaveFinished(bool success,
|
||||||
QTemporaryFile *tempFile, QNetworkReply *modelReply);
|
QFile *tempFile, QNetworkReply *modelReply);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QThread m_hashAndSaveThread;
|
QThread m_hashAndSaveThread;
|
||||||
@ -99,7 +99,7 @@ private Q_SLOTS:
|
|||||||
void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||||
void handleModelDownloadFinished();
|
void handleModelDownloadFinished();
|
||||||
void handleHashAndSaveFinished(bool success,
|
void handleHashAndSaveFinished(bool success,
|
||||||
QTemporaryFile *tempFile, QNetworkReply *modelReply);
|
QFile *tempFile, QNetworkReply *modelReply);
|
||||||
void handleReadyRead();
|
void handleReadyRead();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
@ -110,18 +110,20 @@ Q_SIGNALS:
|
|||||||
void hasNewerReleaseChanged();
|
void hasNewerReleaseChanged();
|
||||||
void downloadLocalModelsPathChanged();
|
void downloadLocalModelsPathChanged();
|
||||||
void requestHashAndSave(const QString &hash, const QString &saveFilePath,
|
void requestHashAndSave(const QString &hash, const QString &saveFilePath,
|
||||||
QTemporaryFile *tempFile, QNetworkReply *modelReply);
|
QFile *tempFile, QNetworkReply *modelReply);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void parseModelsJsonFile(const QByteArray &jsonData);
|
void parseModelsJsonFile(const QByteArray &jsonData);
|
||||||
void parseReleaseJsonFile(const QByteArray &jsonData);
|
void parseReleaseJsonFile(const QByteArray &jsonData);
|
||||||
|
QString incompleteDownloadPath(const QString &modelFile);
|
||||||
|
|
||||||
HashAndSaveFile *m_hashAndSave;
|
HashAndSaveFile *m_hashAndSave;
|
||||||
QMap<QString, ModelInfo> m_modelMap;
|
QMap<QString, ModelInfo> m_modelMap;
|
||||||
QMap<QString, ReleaseInfo> m_releaseMap;
|
QMap<QString, ReleaseInfo> m_releaseMap;
|
||||||
QNetworkAccessManager m_networkManager;
|
QNetworkAccessManager m_networkManager;
|
||||||
QMap<QNetworkReply*, QTemporaryFile*> m_activeDownloads;
|
QMap<QNetworkReply*, QFile*> m_activeDownloads;
|
||||||
QString m_downloadLocalModelsPath;
|
QString m_downloadLocalModelsPath;
|
||||||
|
QDateTime m_startTime;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit Download();
|
explicit Download();
|
||||||
|
Loading…
Reference in New Issue
Block a user