summaryrefslogtreecommitdiffstats
path: root/logic/net
diff options
context:
space:
mode:
Diffstat (limited to 'logic/net')
-rw-r--r--logic/net/ByteArrayDownload.cpp65
-rw-r--r--logic/net/ByteArrayDownload.h24
-rw-r--r--logic/net/CacheDownload.cpp136
-rw-r--r--logic/net/CacheDownload.h34
-rw-r--r--logic/net/Download.h54
-rw-r--r--logic/net/DownloadJob.cpp222
-rw-r--r--logic/net/DownloadJob.h124
-rw-r--r--logic/net/FileDownload.cpp112
-rw-r--r--logic/net/FileDownload.h35
-rw-r--r--logic/net/ForgeXzDownload.cpp279
-rw-r--r--logic/net/ForgeXzDownload.h35
-rw-r--r--logic/net/HttpMetaCache.cpp165
-rw-r--r--logic/net/HttpMetaCache.h39
-rw-r--r--logic/net/LoginTask.cpp285
-rw-r--r--logic/net/LoginTask.h67
15 files changed, 1421 insertions, 255 deletions
diff --git a/logic/net/ByteArrayDownload.cpp b/logic/net/ByteArrayDownload.cpp
new file mode 100644
index 00000000..ba771eef
--- /dev/null
+++ b/logic/net/ByteArrayDownload.cpp
@@ -0,0 +1,65 @@
+#include "ByteArrayDownload.h"
+#include "MultiMC.h"
+#include <logger/QsLog.h>
+
+ByteArrayDownload::ByteArrayDownload(QUrl url) : Download()
+{
+ m_url = url;
+ m_status = Job_NotStarted;
+}
+
+void ByteArrayDownload::start()
+{
+ QLOG_INFO() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ auto worker = MMC->qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void ByteArrayDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ emit progress(index_within_job, bytesReceived, bytesTotal);
+}
+
+void ByteArrayDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ QLOG_ERROR() << "Error getting URL:" << m_url.toString().toLocal8Bit()
+ << "Network error: " << error;
+ m_status = Job_Failed;
+}
+
+void ByteArrayDownload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ m_data = m_reply->readAll();
+ m_reply.reset();
+ emit succeeded(index_within_job);
+ return;
+ }
+ // else the download failed
+ else
+ {
+ m_reply.reset();
+ emit failed(index_within_job);
+ return;
+ }
+}
+
+void ByteArrayDownload::downloadReadyRead()
+{
+ // ~_~
+}
diff --git a/logic/net/ByteArrayDownload.h b/logic/net/ByteArrayDownload.h
new file mode 100644
index 00000000..cfc6a8d0
--- /dev/null
+++ b/logic/net/ByteArrayDownload.h
@@ -0,0 +1,24 @@
+#pragma once
+#include "Download.h"
+
+class ByteArrayDownload: public Download
+{
+ Q_OBJECT
+public:
+ ByteArrayDownload(QUrl url);
+
+public:
+ /// if not saving to file, downloaded data is placed here
+ QByteArray m_data;
+
+public slots:
+ virtual void start();
+
+protected slots:
+ void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ void downloadError(QNetworkReply::NetworkError error);
+ void downloadFinished();
+ void downloadReadyRead();
+};
+
+typedef std::shared_ptr<ByteArrayDownload> ByteArrayDownloadPtr;
diff --git a/logic/net/CacheDownload.cpp b/logic/net/CacheDownload.cpp
new file mode 100644
index 00000000..309eb345
--- /dev/null
+++ b/logic/net/CacheDownload.cpp
@@ -0,0 +1,136 @@
+#include "MultiMC.h"
+#include "CacheDownload.h"
+#include <pathutils.h>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QDateTime>
+#include <logger/QsLog.h>
+
+CacheDownload::CacheDownload(QUrl url, MetaEntryPtr entry)
+ : Download(), md5sum(QCryptographicHash::Md5)
+{
+ m_url = url;
+ m_entry = entry;
+ m_target_path = entry->getFullPath();
+ m_status = Job_NotStarted;
+ m_opened_for_saving = false;
+}
+
+void CacheDownload::start()
+{
+ if (!m_entry->stale)
+ {
+ emit succeeded(index_within_job);
+ return;
+ }
+ m_output_file.setFileName(m_target_path);
+ // if there already is a file and md5 checking is in effect and it can be opened
+ if (!ensureFilePathExists(m_target_path))
+ {
+ emit failed(index_within_job);
+ return;
+ }
+ QLOG_INFO() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+ if(m_entry->remote_changed_timestamp.size())
+ request.setRawHeader(QString("If-Modified-Since").toLatin1(), m_entry->remote_changed_timestamp.toLatin1());
+ if(m_entry->etag.size())
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->etag.toLatin1());
+
+ request.setHeader(QNetworkRequest::UserAgentHeader,"MultiMC/5.0 (Cached)");
+
+ auto worker = MMC->qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void CacheDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ emit progress(index_within_job, bytesReceived, bytesTotal);
+}
+
+void CacheDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ QLOG_ERROR() << "Failed" << m_url.toString() << "with reason" << error;
+ m_status = Job_Failed;
+}
+void CacheDownload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+
+ // nothing went wrong...
+ m_status = Job_Finished;
+ if (m_opened_for_saving)
+ {
+ // save the data to the downloadable if we aren't saving to file
+ m_output_file.close();
+ m_entry->md5sum = md5sum.result().toHex().constData();
+ }
+ else
+ {
+ if (m_output_file.open(QIODevice::ReadOnly))
+ {
+ m_entry->md5sum =
+ QCryptographicHash::hash(m_output_file.readAll(), QCryptographicHash::Md5)
+ .toHex()
+ .constData();
+ m_output_file.close();
+ }
+ }
+ QFileInfo output_file_info(m_target_path);
+
+ m_entry->etag = m_reply->rawHeader("ETag").constData();
+ if(m_reply->hasRawHeader("Last-Modified"))
+ {
+ m_entry->remote_changed_timestamp = m_reply->rawHeader("Last-Modified").constData();
+ }
+ m_entry->local_changed_timestamp =
+ output_file_info.lastModified().toUTC().toMSecsSinceEpoch();
+ m_entry->stale = false;
+ MMC->metacache()->updateEntry(m_entry);
+
+ m_reply.reset();
+ emit succeeded(index_within_job);
+ return;
+ }
+ // else the download failed
+ else
+ {
+ m_output_file.close();
+ m_output_file.remove();
+ m_reply.reset();
+ emit failed(index_within_job);
+ return;
+ }
+}
+
+void CacheDownload::downloadReadyRead()
+{
+ if (!m_opened_for_saving)
+ {
+ if (!m_output_file.open(QIODevice::WriteOnly))
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(index_within_job);
+ return;
+ }
+ m_opened_for_saving = true;
+ }
+ QByteArray ba = m_reply->readAll();
+ md5sum.addData(ba);
+ m_output_file.write(ba);
+}
diff --git a/logic/net/CacheDownload.h b/logic/net/CacheDownload.h
new file mode 100644
index 00000000..295391b1
--- /dev/null
+++ b/logic/net/CacheDownload.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "Download.h"
+#include "HttpMetaCache.h"
+#include <QFile>
+#include <qcryptographichash.h>
+
+class CacheDownload : public Download
+{
+ Q_OBJECT
+public:
+ MetaEntryPtr m_entry;
+ /// is the saving file already open?
+ bool m_opened_for_saving;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+ /// this is the output file, if any
+ QFile m_output_file;
+ /// the hash-as-you-download
+ QCryptographicHash md5sum;
+public:
+ explicit CacheDownload(QUrl url, MetaEntryPtr entry);
+
+protected slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+public slots:
+ virtual void start();
+};
+
+typedef std::shared_ptr<CacheDownload> CacheDownloadPtr;
diff --git a/logic/net/Download.h b/logic/net/Download.h
new file mode 100644
index 00000000..ca4bee9f
--- /dev/null
+++ b/logic/net/Download.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <QObject>
+#include <QUrl>
+#include <memory>
+#include <QNetworkReply>
+
+enum JobStatus
+{
+ Job_NotStarted,
+ Job_InProgress,
+ Job_Finished,
+ Job_Failed
+};
+
+class Download : public QObject
+{
+ Q_OBJECT
+protected:
+ explicit Download() : QObject(0) {};
+
+public:
+ virtual ~Download() {};
+
+public:
+ /// the network reply
+ std::shared_ptr<QNetworkReply> m_reply;
+
+ /// source URL
+ QUrl m_url;
+
+ /// The file's status
+ JobStatus m_status;
+
+ /// index within the parent job
+ int index_within_job = 0;
+
+signals:
+ void started(int index);
+ void progress(int index, qint64 current, qint64 total);
+ void succeeded(int index);
+ void failed(int index);
+
+protected slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0;
+ virtual void downloadError(QNetworkReply::NetworkError error) = 0;
+ virtual void downloadFinished() = 0;
+ virtual void downloadReadyRead() = 0;
+
+public slots:
+ virtual void start() = 0;
+};
+
+typedef std::shared_ptr<Download> DownloadPtr;
diff --git a/logic/net/DownloadJob.cpp b/logic/net/DownloadJob.cpp
index cad9ae72..fa3e655e 100644
--- a/logic/net/DownloadJob.cpp
+++ b/logic/net/DownloadJob.cpp
@@ -1,181 +1,135 @@
#include "DownloadJob.h"
#include "pathutils.h"
#include "MultiMC.h"
+#include "FileDownload.h"
+#include "ByteArrayDownload.h"
+#include "CacheDownload.h"
-Download::Download (QUrl url, QString target_path, QString expected_md5 )
- :Job()
-{
- m_url = url;
- m_target_path = target_path;
- m_expected_md5 = expected_md5;
-
- m_check_md5 = m_expected_md5.size();
- m_save_to_file = m_target_path.size();
- m_status = Job_NotStarted;
- m_opened_for_saving = false;
-}
-
-void Download::start()
-{
- if ( m_save_to_file )
- {
- QString filename = m_target_path;
- m_output_file.setFileName ( filename );
- // if there already is a file and md5 checking is in effect and it can be opened
- if ( m_output_file.exists() && m_output_file.open ( QIODevice::ReadOnly ) )
- {
- // check the md5 against the expected one
- QString hash = QCryptographicHash::hash ( m_output_file.readAll(), QCryptographicHash::Md5 ).toHex().constData();
- m_output_file.close();
- // skip this file if they match
- if ( m_check_md5 && hash == m_expected_md5 )
- {
- qDebug() << "Skipping " << m_url.toString() << ": md5 match.";
- emit succeeded(index_within_job);
- return;
- }
- else
- {
- m_expected_md5 = hash;
- }
- }
- if(!ensureFilePathExists(filename))
- {
- emit failed(index_within_job);
- return;
- }
- }
- qDebug() << "Downloading " << m_url.toString();
- QNetworkRequest request ( m_url );
- request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1());
-
- auto worker = MMC->qnam();
- QNetworkReply * rep = worker->get ( request );
-
- m_reply = QSharedPointer<QNetworkReply> ( rep, &QObject::deleteLater );
- connect ( rep, SIGNAL ( downloadProgress ( qint64,qint64 ) ), SLOT ( downloadProgress ( qint64,qint64 ) ) );
- connect ( rep, SIGNAL ( finished() ), SLOT ( downloadFinished() ) );
- connect ( rep, SIGNAL ( error ( QNetworkReply::NetworkError ) ), SLOT ( downloadError ( QNetworkReply::NetworkError ) ) );
- connect ( rep, SIGNAL ( readyRead() ), SLOT ( downloadReadyRead() ) );
-}
-
-void Download::downloadProgress ( qint64 bytesReceived, qint64 bytesTotal )
-{
- emit progress (index_within_job, bytesReceived, bytesTotal );
-}
+#include <logger/QsLog.h>
-void Download::downloadError ( QNetworkReply::NetworkError error )
+ByteArrayDownloadPtr DownloadJob::addByteArrayDownload(QUrl url)
{
- // error happened during download.
- // TODO: log the reason why
- m_status = Job_Failed;
+ ByteArrayDownloadPtr ptr(new ByteArrayDownload(url));
+ ptr->index_within_job = downloads.size();
+ downloads.append(ptr);
+ parts_progress.append(part_info());
+ total_progress++;
+ return ptr;
}
-void Download::downloadFinished()
+FileDownloadPtr DownloadJob::addFileDownload(QUrl url, QString rel_target_path)
{
- // if the download succeeded
- if ( m_status != Job_Failed )
- {
- // nothing went wrong...
- m_status = Job_Finished;
- // save the data to the downloadable if we aren't saving to file
- if ( !m_save_to_file )
- {
- m_data = m_reply->readAll();
- }
- else
- {
- m_output_file.close();
- }
-
- //TODO: check md5 here!
- m_reply.clear();
- emit succeeded(index_within_job);
- return;
- }
- // else the download failed
- else
- {
- if ( m_save_to_file )
- {
- m_output_file.close();
- m_output_file.remove();
- }
- m_reply.clear();
- emit failed(index_within_job);
- return;
- }
+ FileDownloadPtr ptr(new FileDownload(url, rel_target_path));
+ ptr->index_within_job = downloads.size();
+ downloads.append(ptr);
+ parts_progress.append(part_info());
+ total_progress++;
+ return ptr;
}
-void Download::downloadReadyRead()
+CacheDownloadPtr DownloadJob::addCacheDownload(QUrl url, MetaEntryPtr entry)
{
- if( m_save_to_file )
- {
- if(!m_opened_for_saving)
- {
- if ( !m_output_file.open ( QIODevice::WriteOnly ) )
- {
- /*
- * Can't open the file... the job failed
- */
- m_reply->abort();
- emit failed(index_within_job);
- return;
- }
- m_opened_for_saving = true;
- }
- m_output_file.write ( m_reply->readAll() );
- }
+ CacheDownloadPtr ptr(new CacheDownload(url, entry));
+ ptr->index_within_job = downloads.size();
+ downloads.append(ptr);
+ parts_progress.append(part_info());
+ total_progress++;
+ return ptr;
}
-DownloadPtr DownloadJob::add ( QUrl url, QString rel_target_path, QString expected_md5 )
+ForgeXzDownloadPtr DownloadJob::addForgeXzDownload(QUrl url, MetaEntryPtr entry)
{
- DownloadPtr ptr (new Download(url, rel_target_path, expected_md5));
+ ForgeXzDownloadPtr ptr(new ForgeXzDownload(url, entry));
ptr->index_within_job = downloads.size();
downloads.append(ptr);
+ parts_progress.append(part_info());
+ total_progress++;
return ptr;
}
-void DownloadJob::partSucceeded ( int index )
+void DownloadJob::partSucceeded(int index)
{
+ // do progress. all slots are 1 in size at least
+ auto &slot = parts_progress[index];
+ partProgress(index, slot.total_progress, slot.total_progress);
+
num_succeeded++;
- if(num_failed + num_succeeded == downloads.size())
+ QLOG_INFO() << m_job_name.toLocal8Bit() << "progress:" << num_succeeded << "/"
+ << downloads.size();
+ if (num_failed + num_succeeded == downloads.size())
{
- if(num_failed)
+ if (num_failed)
{
- qDebug() << "Download JOB failed: " << this;
+ QLOG_ERROR() << m_job_name.toLocal8Bit() << "failed.";
emit failed();
}
else
{
- qDebug() << "Download JOB succeeded: " << this;
+ QLOG_INFO() << m_job_name.toLocal8Bit() << "succeeded.";
emit succeeded();
}
}
}
-void DownloadJob::partFailed ( int index )
+void DownloadJob::partFailed(int index)
{
- num_failed++;
- if(num_failed + num_succeeded == downloads.size())
+ auto &slot = parts_progress[index];
+ if (slot.failures == 3)
{
- qDebug() << "Download JOB failed: " << this;
- emit failed();
+ QLOG_ERROR() << "Part" << index << "failed 3 times (" << downloads[index]->m_url << ")";
+ num_failed++;
+ if (num_failed + num_succeeded == downloads.size())
+ {
+ QLOG_ERROR() << m_job_name.toLocal8Bit() << "failed.";
+ emit failed();
+ }
+ }
+ else
+ {
+ QLOG_ERROR() << "Part" << index << "failed, restarting (" << downloads[index]->m_url
+ << ")";
+ // restart the job
+ slot.failures++;
+ downloads[index]->start();
}
}
-void DownloadJob::partProgress ( int index, qint64 bytesReceived, qint64 bytesTotal )
+void DownloadJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
{
- // PROGRESS? DENIED!
-}
+ auto &slot = parts_progress[index];
+
+ current_progress -= slot.current_progress;
+ slot.current_progress = bytesReceived;
+ current_progress += slot.current_progress;
+ total_progress -= slot.total_progress;
+ slot.total_progress = bytesTotal;
+ total_progress += slot.total_progress;
+ emit progress(current_progress, total_progress);
+}
void DownloadJob::start()
{
- qDebug() << "Download JOB started: " << this;
- for(auto iter: downloads)
+ QLOG_INFO() << m_job_name.toLocal8Bit() << " started.";
+ for (auto iter : downloads)
{
- connect(iter.data(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int)));
+ connect(iter.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int)));
+ connect(iter.get(), SIGNAL(failed(int)), SLOT(partFailed(int)));
+ connect(iter.get(), SIGNAL(progress(int, qint64, qint64)),
+ SLOT(partProgress(int, qint64, qint64)));
iter->start();
}
}
+
+QStringList DownloadJob::getFailedFiles()
+{
+ QStringList failed;
+ for (auto download : downloads)
+ {
+ if (download->m_status == Job_Failed)
+ {
+ failed.push_back(download->m_url.toString());
+ }
+ }
+ return failed;
+}
diff --git a/logic/net/DownloadJob.h b/logic/net/DownloadJob.h
index adeef646..91b014ad 100644
--- a/logic/net/DownloadJob.h
+++ b/logic/net/DownloadJob.h
@@ -1,98 +1,31 @@
#pragma once
#include <QtNetwork>
+#include "Download.h"
+#include "ByteArrayDownload.h"
+#include "FileDownload.h"
+#include "CacheDownload.h"
+#include "HttpMetaCache.h"
+#include "ForgeXzDownload.h"
+#include "logic/tasks/ProgressProvider.h"
class DownloadJob;
-class Download;
-typedef QSharedPointer<DownloadJob> DownloadJobPtr;
-typedef QSharedPointer<Download> DownloadPtr;
-
-enum JobStatus
-{
- Job_NotStarted,
- Job_InProgress,
- Job_Finished,
- Job_Failed
-};
-
-class Job : public QObject
-{
- Q_OBJECT
-protected:
- explicit Job(): QObject(0){};
-public:
- virtual ~Job() {};
-
-public slots:
- virtual void start() = 0;
-};
-
-class Download: public Job
-{
- friend class DownloadJob;
- Q_OBJECT
-protected:
- Download(QUrl url, QString rel_target_path = QString(), QString expected_md5 = QString());
-public:
- /// the network reply
- QSharedPointer<QNetworkReply> m_reply;
- /// source URL
- QUrl m_url;
-
- /// if true, check the md5sum against a provided md5sum
- /// also, if a file exists, perform an md5sum first and don't download only if they don't match
- bool m_check_md5;
- /// the expected md5 checksum
- QString m_expected_md5;
-
- /// save to file?
- bool m_save_to_file;
- /// is the saving file already open?
- bool m_opened_for_saving;
- /// if saving to file, use the one specified in this string
- QString m_target_path;
- /// this is the output file, if any
- QFile m_output_file;
- /// if not saving to file, downloaded data is placed here
- QByteArray m_data;
-
- int currentProgress = 0;
- int totalProgress = 0;
-
- /// The file's status
- JobStatus m_status;
-
- int index_within_job = 0;
-signals:
- void started(int index);
- void progress(int index, qint64 current, qint64 total);
- void succeeded(int index);
- void failed(int index);
-public slots:
- virtual void start();
-
-private slots:
- void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);;
- void downloadError(QNetworkReply::NetworkError error);
- void downloadFinished();
- void downloadReadyRead();
-};
+typedef std::shared_ptr<DownloadJob> DownloadJobPtr;
/**
* A single file for the downloader/cache to process.
*/
-class DownloadJob : public Job
+class DownloadJob : public ProgressProvider
{
Q_OBJECT
public:
- explicit DownloadJob()
- :Job(){};
- explicit DownloadJob(QUrl url, QString rel_target_path = QString(), QString expected_md5 = QString())
- :Job()
- {
- add(url, rel_target_path, expected_md5);
- };
+ explicit DownloadJob(QString job_name)
+ :ProgressProvider(), m_job_name(job_name){};
+
+ ByteArrayDownloadPtr addByteArrayDownload(QUrl url);
+ FileDownloadPtr addFileDownload(QUrl url, QString rel_target_path);
+ CacheDownloadPtr addCacheDownload(QUrl url, MetaEntryPtr entry);
+ ForgeXzDownloadPtr addForgeXzDownload(QUrl url, MetaEntryPtr entry);
- DownloadPtr add(QUrl url, QString rel_target_path = QString(), QString expected_md5 = QString());
DownloadPtr operator[](int index)
{
return downloads[index];
@@ -107,6 +40,20 @@ public:
{
return downloads.size();
}
+ virtual void getProgress(qint64& current, qint64& total)
+ {
+ current = current_progress;
+ total = total_progress;
+ };
+ virtual QString getStatus() const
+ {
+ return m_job_name;
+ };
+ virtual bool isRunning() const
+ {
+ return m_running;
+ };
+ QStringList getFailedFiles();
signals:
void started();
void progress(qint64 current, qint64 total);
@@ -119,8 +66,19 @@ private slots:
void partSucceeded(int index);
void partFailed(int index);
private:
+ struct part_info
+ {
+ qint64 current_progress = 0;
+ qint64 total_progress = 1;
+ int failures = 0;
+ };
+ QString m_job_name;
QList<DownloadPtr> downloads;
+ QList<part_info> parts_progress;
+ qint64 current_progress = 0;
+ qint64 total_progress = 0;
int num_succeeded = 0;
int num_failed = 0;
+ bool m_running = false;
};
diff --git a/logic/net/FileDownload.cpp b/logic/net/FileDownload.cpp
new file mode 100644
index 00000000..3f38b0fa
--- /dev/null
+++ b/logic/net/FileDownload.cpp
@@ -0,0 +1,112 @@
+#include "MultiMC.h"
+#include "FileDownload.h"
+#include <pathutils.h>
+#include <QCryptographicHash>
+#include <logger/QsLog.h>
+
+
+FileDownload::FileDownload ( QUrl url, QString target_path )
+ :Download()
+{
+ m_url = url;
+ m_target_path = target_path;
+ m_check_md5 = false;
+ m_status = Job_NotStarted;
+ m_opened_for_saving = false;
+}
+
+void FileDownload::start()
+{
+ QString filename = m_target_path;
+ m_output_file.setFileName ( filename );
+ // if there already is a file and md5 checking is in effect and it can be opened
+ if ( m_output_file.exists() && m_output_file.open ( QIODevice::ReadOnly ) )
+ {
+ // check the md5 against the expected one
+ QString hash = QCryptographicHash::hash ( m_output_file.readAll(), QCryptographicHash::Md5 ).toHex().constData();
+ m_output_file.close();
+ // skip this file if they match
+ if ( m_check_md5 && hash == m_expected_md5 )
+ {
+ QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match.";
+ emit succeeded(index_within_job);
+ return;
+ }
+ else
+ {
+ m_expected_md5 = hash;
+ }
+ }
+ if(!ensureFilePathExists(filename))
+ {
+ emit failed(index_within_job);
+ return;
+ }
+
+ QLOG_INFO() << "Downloading " << m_url.toString();
+ QNetworkRequest request ( m_url );
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1());
+ request.setHeader(QNetworkRequest::UserAgentHeader,"MultiMC/5.0 (Uncached)");
+
+ auto worker = MMC->qnam();
+ QNetworkReply * rep = worker->get ( request );
+
+ m_reply = std::shared_ptr<QNetworkReply> ( rep );
+ connect ( rep, SIGNAL ( downloadProgress ( qint64,qint64 ) ), SLOT ( downloadProgress ( qint64,qint64 ) ) );
+ connect ( rep, SIGNAL ( finished() ), SLOT ( downloadFinished() ) );
+ connect ( rep, SIGNAL ( error ( QNetworkReply::NetworkError ) ), SLOT ( downloadError ( QNetworkReply::NetworkError ) ) );
+ connect ( rep, SIGNAL ( readyRead() ), SLOT ( downloadReadyRead() ) );
+}
+
+void FileDownload::downloadProgress ( qint64 bytesReceived, qint64 bytesTotal )
+{
+ emit progress (index_within_job, bytesReceived, bytesTotal );
+}
+
+void FileDownload::downloadError ( QNetworkReply::NetworkError error )
+{
+ // error happened during download.
+ // TODO: log the reason why
+ m_status = Job_Failed;
+}
+
+void FileDownload::downloadFinished()
+{
+ // if the download succeeded
+ if ( m_status != Job_Failed )
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ m_output_file.close();
+
+ m_reply.reset();
+ emit succeeded(index_within_job);
+ return;
+ }
+ // else the download failed
+ else
+ {
+ m_output_file.close();
+ m_reply.reset();
+ emit failed(index_within_job);
+ return;
+ }
+}
+
+void FileDownload::downloadReadyRead()
+{
+ if(!m_opened_for_saving)
+ {
+ if ( !m_output_file.open ( QIODevice::WriteOnly ) )
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(index_within_job);
+ return;
+ }
+ m_opened_for_saving = true;
+ }
+ m_output_file.write ( m_reply->readAll() );
+}
diff --git a/logic/net/FileDownload.h b/logic/net/FileDownload.h
new file mode 100644
index 00000000..9abb590d
--- /dev/null
+++ b/logic/net/FileDownload.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "Download.h"
+#include <QFile>
+
+class FileDownload : public Download
+{
+ Q_OBJECT
+public:
+ /// if true, check the md5sum against a provided md5sum
+ /// also, if a file exists, perform an md5sum first and don't download only if they don't match
+ bool m_check_md5;
+ /// the expected md5 checksum
+ QString m_expected_md5;
+ /// is the saving file already open?
+ bool m_opened_for_saving;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+ /// this is the output file, if any
+ QFile m_output_file;
+
+public:
+ explicit FileDownload(QUrl url, QString target_path);
+
+protected slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+public slots:
+ virtual void start();
+};
+
+typedef std::shared_ptr<FileDownload> FileDownloadPtr;
diff --git a/logic/net/ForgeXzDownload.cpp b/logic/net/ForgeXzDownload.cpp
new file mode 100644
index 00000000..0e5287d8
--- /dev/null
+++ b/logic/net/ForgeXzDownload.cpp
@@ -0,0 +1,279 @@
+#include "MultiMC.h"
+#include "ForgeXzDownload.h"
+#include <pathutils.h>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QDateTime>
+#include <logger/QsLog.h>
+
+ForgeXzDownload::ForgeXzDownload(QUrl url, MetaEntryPtr entry)
+ : Download()
+{
+ QString urlstr = url.toString();
+ urlstr.append(".pack.xz");
+ m_url = QUrl(urlstr);
+ m_entry = entry;
+ m_target_path = entry->getFullPath();
+ m_status = Job_NotStarted;
+ m_opened_for_saving = false;
+}
+
+void ForgeXzDownload::start()
+{
+ if (!m_entry->stale)
+ {
+ emit succeeded(index_within_job);
+ return;
+ }
+ // can we actually create the real, final file?
+ if (!ensureFilePathExists(m_target_path))
+ {
+ emit failed(index_within_job);
+ return;
+ }
+ QLOG_INFO() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->etag.toLatin1());
+ request.setHeader(QNetworkRequest::UserAgentHeader,"MultiMC/5.0 (Cached)");
+
+ auto worker = MMC->qnam();
+ QNetworkReply *rep = worker->get(request);
+
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgress(qint64, qint64)));
+ connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
+}
+
+void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ emit progress(index_within_job, bytesReceived, bytesTotal);
+}
+
+void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ // TODO: log the reason why
+ m_status = Job_Failed;
+}
+
+void ForgeXzDownload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ if (m_opened_for_saving)
+ {
+ // we actually downloaded something! process and isntall it
+ decompressAndInstall();
+ return;
+ }
+ else
+ {
+ // something bad happened
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ emit failed(index_within_job);
+ return;
+ }
+ }
+ // else the download failed
+ else
+ {
+ m_pack200_xz_file.close();
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ emit failed(index_within_job);
+ return;
+ }
+}
+
+void ForgeXzDownload::downloadReadyRead()
+{
+
+ if (!m_opened_for_saving)
+ {
+ if (!m_pack200_xz_file.open())
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(index_within_job);
+ return;
+ }
+ m_opened_for_saving = true;
+ }
+ m_pack200_xz_file.write(m_reply->readAll());
+}
+
+#include "xz.h"
+#include "unpack200.h"
+#include <stdexcept>
+
+const size_t buffer_size = 8196;
+
+void ForgeXzDownload::decompressAndInstall()
+{
+ // rewind the downloaded temp file
+ m_pack200_xz_file.seek(0);
+ // de-xz'd file
+ QTemporaryFile pack200_file;
+ pack200_file.open();
+
+ bool xz_success = false;
+ // first, de-xz
+ {
+ uint8_t in[buffer_size];
+ uint8_t out[buffer_size];
+ struct xz_buf b;
+ struct xz_dec *s;
+ enum xz_ret ret;
+ xz_crc32_init();
+ xz_crc64_init();
+ s = xz_dec_init(XZ_DYNALLOC, 1 << 26);
+ if (s == nullptr)
+ {
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+ }
+ b.in = in;
+ b.in_pos = 0;
+ b.in_size = 0;
+ b.out = out;
+ b.out_pos = 0;
+ b.out_size = buffer_size;
+ while (!xz_success)
+ {
+ if (b.in_pos == b.in_size)
+ {
+ b.in_size = m_pack200_xz_file.read((char*)in, sizeof(in));
+ b.in_pos = 0;
+ }
+
+ ret = xz_dec_run(s, &b);
+
+ if (b.out_pos == sizeof(out))
+ {
+ if (pack200_file.write((char*)out, b.out_pos) != b.out_pos)
+ {
+ // msg = "Write error\n";
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+ }
+
+ b.out_pos = 0;
+ }
+
+ if (ret == XZ_OK)
+ continue;
+
+ if (ret == XZ_UNSUPPORTED_CHECK)
+ {
+ // unsupported check. this is OK, but we should log this
+ continue;
+ }
+
+ if (pack200_file.write((char*)out, b.out_pos) != b.out_pos )
+ {
+ // write error
+ pack200_file.close();
+ xz_dec_end(s);
+ return;
+ }
+
+ switch (ret)
+ {
+ case XZ_STREAM_END:
+ xz_dec_end(s);
+ xz_success = true;
+ break;
+
+ case XZ_MEM_ERROR:
+ QLOG_ERROR() << "Memory allocation failed\n";
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+
+ case XZ_MEMLIMIT_ERROR:
+ QLOG_ERROR() << "Memory usage limit reached\n";
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+
+ case XZ_FORMAT_ERROR:
+ QLOG_ERROR() << "Not a .xz file\n";
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+
+ case XZ_OPTIONS_ERROR:
+ QLOG_ERROR() << "Unsupported options in the .xz headers\n";
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+
+ case XZ_DATA_ERROR:
+ case XZ_BUF_ERROR:
+ QLOG_ERROR() << "File is corrupt\n";
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+
+ default:
+ QLOG_ERROR() << "Bug!\n";
+ xz_dec_end(s);
+ emit failed(index_within_job);
+ return;
+ }
+ }
+ }
+
+ // revert pack200
+ pack200_file.close();
+ QString pack_name = pack200_file.fileName();
+ try
+ {
+ unpack_200(pack_name.toStdString(), m_target_path.toStdString());
+ }
+ catch(std::runtime_error & err)
+ {
+ QLOG_ERROR() << "Error unpacking " << pack_name.toUtf8() << " : " << err.what();
+ QFile f(m_target_path);
+ if(f.exists())
+ f.remove();
+ emit failed(index_within_job);
+ return;
+ }
+
+ QFile jar_file(m_target_path);
+
+ if (!jar_file.open(QIODevice::ReadOnly))
+ {
+ jar_file.remove();
+ emit failed(index_within_job);
+ return;
+ }
+ m_entry->md5sum = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5)
+ .toHex()
+ .constData();
+ jar_file.close();
+
+ QFileInfo output_file_info(m_target_path);
+ m_entry->etag = m_reply->rawHeader("ETag").constData();
+ m_entry->local_changed_timestamp =
+ output_file_info.lastModified().toUTC().toMSecsSinceEpoch();
+ m_entry->stale = false;
+ MMC->metacache()->updateEntry(m_entry);
+
+ m_reply.reset();
+ emit succeeded(index_within_job);
+}
diff --git a/logic/net/ForgeXzDownload.h b/logic/net/ForgeXzDownload.h
new file mode 100644
index 00000000..5d677947
--- /dev/null
+++ b/logic/net/ForgeXzDownload.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "Download.h"
+#include "HttpMetaCache.h"
+#include <QFile>
+#include <QTemporaryFile>
+
+class ForgeXzDownload : public Download
+{
+ Q_OBJECT
+public:
+ MetaEntryPtr m_entry;
+ /// is the saving file already open?
+ bool m_opened_for_saving;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+ /// this is the output file, if any
+ QTemporaryFile m_pack200_xz_file;
+
+public:
+ explicit ForgeXzDownload(QUrl url, MetaEntryPtr entry);
+
+protected slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+public slots:
+ virtual void start();
+private:
+ void decompressAndInstall();
+};
+
+typedef std::shared_ptr<ForgeXzDownload> ForgeXzDownloadPtr;
diff --git a/logic/net/HttpMetaCache.cpp b/logic/net/HttpMetaCache.cpp
index 87741dc9..5ba5b98d 100644
--- a/logic/net/HttpMetaCache.cpp
+++ b/logic/net/HttpMetaCache.cpp
@@ -1,38 +1,130 @@
+#include "MultiMC.h"
#include "HttpMetaCache.h"
#include <pathutils.h>
+
+#include <QFileInfo>
#include <QFile>
-#include <qjsondocument.h>
-#include <qjsonarray.h>
-#include <qjsonobject.h>
-#include <qfileinfo.h>
-#include <qtemporaryfile.h>
-#include <qsavefile.h>
+#include <QTemporaryFile>
+#include <QSaveFile>
+#include <QDateTime>
+#include <QCryptographicHash>
+
+#include <logger/QsLog.h>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+
+QString MetaEntry::getFullPath()
+{
+ return PathCombine(MMC->metacache()->getBasePath(base), path);
+}
+
HttpMetaCache::HttpMetaCache(QString path)
+ :QObject()
{
m_index_file = path;
+ saveBatchingTimer.setSingleShot(true);
+ saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer);
+ connect(&saveBatchingTimer,SIGNAL(timeout()),SLOT(SaveNow()));
}
+
HttpMetaCache::~HttpMetaCache()
{
- Save();
+ saveBatchingTimer.stop();
+ SaveNow();
}
-void HttpMetaCache::addEntry ( QString base, QString resource_path, QString etag )
+MetaEntryPtr HttpMetaCache::getEntry ( QString base, QString resource_path )
{
// no base. no base path. can't store
if(!m_entries.contains(base))
- return;
- QString real_path = PathCombine(m_entries[base].base_path, resource_path);
+ {
+ // TODO: log problem
+ return MetaEntryPtr();
+ }
+ EntryMap & map = m_entries[base];
+ if(map.entry_list.contains(resource_path))
+ {
+ return map.entry_list[resource_path];
+ }
+ return MetaEntryPtr();
+}
+
+MetaEntryPtr HttpMetaCache::resolveEntry ( QString base, QString resource_path, QString expected_etag )
+{
+ auto entry = getEntry(base, resource_path);
+ // it's not present? generate a default stale entry
+ if(!entry)
+ {
+ return staleEntry(base, resource_path);
+ }
+
+ auto & selected_base = m_entries[base];
+ QString real_path = PathCombine(selected_base.base_path, resource_path);
QFileInfo finfo(real_path);
- // just ignore it, it's garbage if it's not a proper file
+ // is the file really there? if not -> stale
if(!finfo.isFile() || !finfo.isReadable())
{
- // TODO: log problem
- return;
+ // if the file doesn't exist, we disown the entry
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
+ }
+
+ if(!expected_etag.isEmpty() && expected_etag != entry->etag)
+ {
+ // if the etag doesn't match expected, we disown the entry
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
}
- Save();
+ // if the file changed, check md5sum
+ qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch();
+ if(file_last_changed != entry->local_changed_timestamp)
+ {
+ QFile input(real_path);
+ input.open(QIODevice::ReadOnly);
+ QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData();
+ if(entry->md5sum != md5sum)
+ {
+ selected_base.entry_list.remove(resource_path);
+ return staleEntry(base, resource_path);
+ }
+ // md5sums matched... keep entry and save the new state to file
+ entry->local_changed_timestamp = file_last_changed;
+ SaveEventually();
+ }
+
+ // entry passed all the checks we cared about.
+ return entry;
+}
+
+bool HttpMetaCache::updateEntry ( MetaEntryPtr stale_entry )
+{
+ if(!m_entries.contains(stale_entry->base))
+ {
+ QLOG_ERROR() << "Cannot add entry with unknown base: " << stale_entry->base.toLocal8Bit();
+ return false;
+ }
+ if(stale_entry->stale)
+ {
+ QLOG_ERROR() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
+ return false;
+ }
+ m_entries[stale_entry->base].entry_list[stale_entry->path] = stale_entry;
+ SaveEventually();
+ return true;
+}
+
+MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path)
+{
+ auto foo = new MetaEntry;
+ foo->base = base;
+ foo->path = resource_path;
+ foo->stale = true;
+ return MetaEntryPtr(foo);
}
void HttpMetaCache::addBase ( QString base, QString base_root )
@@ -46,6 +138,16 @@ void HttpMetaCache::addBase ( QString base, QString base_root )
m_entries[base] = foo;
}
+QString HttpMetaCache::getBasePath ( QString base )
+{
+ if(m_entries.contains(base))
+ {
+ return m_entries[base].base_path;
+ }
+ return QString();
+}
+
+
void HttpMetaCache::Load()
{
QFile index(m_index_file);
@@ -65,12 +167,12 @@ void HttpMetaCache::Load()
// read the entry array
auto entries_val =root.value("entries");
- if(!version_val.isArray())
+ if(!entries_val.isArray())
return;
- QJsonArray array = json.array();
+ QJsonArray array = entries_val.toArray();
for(auto element: array)
{
- if(!element.isObject());
+ if(!element.isObject())
return;
auto element_obj = element.toObject();
QString base = element_obj.value("base").toString();
@@ -82,12 +184,22 @@ void HttpMetaCache::Load()
QString path = foo->path = element_obj.value("path").toString();
foo->md5sum = element_obj.value("md5sum").toString();
foo->etag = element_obj.value("etag").toString();
- foo->last_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble();
+ foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble();
+ foo->remote_changed_timestamp = element_obj.value("remote_changed_timestamp").toString();
+ // presumed innocent until closer examination
+ foo->stale = false;
entrymap.entry_list[path] = MetaEntryPtr( foo );
}
}
-void HttpMetaCache::Save()
+void HttpMetaCache::SaveEventually()
+{
+ // reset the save timer
+ saveBatchingTimer.stop();
+ saveBatchingTimer.start(30000);
+}
+
+void HttpMetaCache::SaveNow()
{
QSaveFile tfile(m_index_file);
if(!tfile.open(QIODevice::WriteOnly | QIODevice::Truncate))
@@ -104,7 +216,9 @@ void HttpMetaCache::Save()
entryObj.insert("path", QJsonValue(entry->path));
entryObj.insert("md5sum", QJsonValue(entry->md5sum));
entryObj.insert("etag", QJsonValue(entry->etag));
- entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->last_changed_timestamp)));
+ entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp)));
+ if(!entry->remote_changed_timestamp.isEmpty())
+ entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp));
entriesArr.append(entryObj);
}
}
@@ -118,14 +232,3 @@ void HttpMetaCache::Save()
return;
tfile.commit();
}
-
-
-MetaEntryPtr HttpMetaCache::getEntryForResource ( QString base, QString resource_path )
-{
- if(!m_entries.contains(base))
- return MetaEntryPtr();
- auto & entrymap = m_entries[base];
- if(!entrymap.entry_list.contains(resource_path))
- return MetaEntryPtr();
- return entrymap.entry_list[resource_path];
-}
diff --git a/logic/net/HttpMetaCache.h b/logic/net/HttpMetaCache.h
index 161483ad..557d9298 100644
--- a/logic/net/HttpMetaCache.h
+++ b/logic/net/HttpMetaCache.h
@@ -2,6 +2,7 @@
#include <QString>
#include <QSharedPointer>
#include <QMap>
+#include <qtimer.h>
struct MetaEntry
{
@@ -9,23 +10,46 @@ struct MetaEntry
QString path;
QString md5sum;
QString etag;
- quint64 last_changed_timestamp = 0;
+ qint64 local_changed_timestamp = 0;
+ QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time
+ bool stale = true;
+ QString getFullPath();
};
-typedef QSharedPointer<MetaEntry> MetaEntryPtr;
+typedef std::shared_ptr<MetaEntry> MetaEntryPtr;
-class HttpMetaCache
+class HttpMetaCache : public QObject
{
+ Q_OBJECT
public:
// supply path to the cache index file
HttpMetaCache(QString path);
~HttpMetaCache();
- MetaEntryPtr getEntryForResource(QString base, QString resource_path);
- void addEntry(QString base, QString resource_path, QString etag);
+
+ // get the entry solely from the cache
+ // you probably don't want this, unless you have some specific caching needs.
+ MetaEntryPtr getEntry(QString base, QString resource_path);
+
+ // get the entry from cache and verify that it isn't stale (within reason)
+ MetaEntryPtr resolveEntry(QString base, QString resource_path,
+ QString expected_etag = QString());
+
+ // add a previously resolved stale entry
+ bool updateEntry(MetaEntryPtr stale_entry);
+
void addBase(QString base, QString base_root);
-private:
- void Save();
+
+ // (re)start a timer that calls SaveNow later.
+ void SaveEventually();
void Load();
+ QString getBasePath(QString base);
+public
+slots:
+ void SaveNow();
+
+private:
+ // create a new stale entry, given the parameters
+ MetaEntryPtr staleEntry(QString base, QString resource_path);
struct EntryMap
{
QString base_path;
@@ -33,4 +57,5 @@ private:
};
QMap<QString, EntryMap> m_entries;
QString m_index_file;
+ QTimer saveBatchingTimer;
}; \ No newline at end of file
diff --git a/logic/net/LoginTask.cpp b/logic/net/LoginTask.cpp
new file mode 100644
index 00000000..4098783b
--- /dev/null
+++ b/logic/net/LoginTask.cpp
@@ -0,0 +1,285 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LoginTask.h"
+#include "MultiMC.h"
+#include <settingsobject.h>
+
+#include <QStringList>
+
+#include <QNetworkReply>
+#include <QNetworkRequest>
+
+#include <QUrl>
+#include <QUrlQuery>
+#include <QJsonParseError>
+#include <QJsonObject>
+
+LoginTask::LoginTask(const UserInfo &uInfo, QObject *parent) : Task(parent), uInfo(uInfo)
+{
+}
+
+void LoginTask::executeTask()
+{
+ yggdrasilLogin();
+}
+
+void LoginTask::legacyLogin()
+{
+ setStatus(tr("Logging in..."));
+ auto worker = MMC->qnam();
+ connect(worker.get(), SIGNAL(finished(QNetworkReply *)), this,
+ SLOT(processLegacyReply(QNetworkReply *)));
+
+ QUrl loginURL("https://login.minecraft.net/");
+ QNetworkRequest netRequest(loginURL);
+ netRequest.setHeader(QNetworkRequest::ContentTypeHeader,
+ "application/x-www-form-urlencoded");
+
+ QUrlQuery params;
+ params.addQueryItem("user", uInfo.username);
+ params.addQueryItem("password", uInfo.password);
+ params.addQueryItem("version", "13");
+
+ netReply = worker->post(netRequest, params.query(QUrl::EncodeSpaces).toUtf8());
+}
+
+void LoginTask::parseLegacyReply(QByteArray data)
+{
+ QString responseStr = QString::fromUtf8(data);
+
+ QStringList strings = responseStr.split(":");
+ if (strings.count() >= 4)
+ {
+ // strings[1] is the download ticket. It isn't used anymore.
+ QString username = strings[2];
+ QString sessionID = strings[3];
+ /*
+ struct LoginResponse
+ {
+ QString username;
+ QString session_id;
+ QString player_name;
+ QString player_id;
+ QString client_id;
+ };
+ */
+ result = {username, sessionID, username, QString()};
+ emitSucceeded();
+ }
+ else
+ {
+ if (responseStr.toLower() == "bad login")
+ emitFailed(tr("Invalid username or password."));
+ else if (responseStr.toLower() == "old version")
+ emitFailed(tr("Launcher outdated, please update."));
+ else
+ emitFailed(tr("Login failed: %1").arg(responseStr));
+ }
+}
+
+void LoginTask::processLegacyReply(QNetworkReply *reply)
+{
+ processReply(reply, &LoginTask::parseLegacyReply, &LoginTask::parseLegacyError);
+}
+
+void LoginTask::processYggdrasilReply(QNetworkReply *reply)
+{
+ processReply(reply, &LoginTask::parseYggdrasilReply, &LoginTask::parseYggdrasilError);
+}
+
+void LoginTask::processReply(QNetworkReply *reply, std::function<void (LoginTask*, QByteArray)> parser, std::function<QString (LoginTask*, QNetworkReply*)> errorHandler)
+{
+ if (netReply != reply)
+ return;
+ // Check for errors.
+ switch (reply->error())
+ {
+ case QNetworkReply::NoError:
+ {
+ // Check the response code.
+ int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ switch (responseCode)
+ {
+ case 200:
+ parser(this, reply->readAll());
+ break;
+
+ default:
+ emitFailed(tr("Login failed: Unknown HTTP code %1 encountered.").arg(responseCode));
+ break;
+ }
+
+ break;
+ }
+
+ case QNetworkReply::OperationCanceledError:
+ emitFailed(tr("Login canceled."));
+ break;
+
+ default:
+ emitFailed(errorHandler(this, reply));
+ break;
+ }
+}
+
+QString LoginTask::parseLegacyError(QNetworkReply *reply)
+{
+ int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+ switch (responseCode)
+ {
+ case 403:
+ return tr("Invalid username or password.");
+
+ case 503:
+ return tr("The login servers are currently unavailable. Check "
+ "http://help.mojang.com/ for more info.");
+
+ default:
+ QLOG_DEBUG() << "Login failed with QNetworkReply code:" << reply->error();
+ return tr("Login failed: %1").arg(reply->errorString());
+ }
+}
+
+QString LoginTask::parseYggdrasilError(QNetworkReply *reply)
+{
+ QByteArray data = reply->readAll();
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+
+ // If there are JSON errors fall back to using the legacy error handling using HTTP status codes
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ return parseLegacyError(reply);
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ return parseLegacyError(reply);
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ //QString error = root.value("error").toString();
+ QString errorMessage = root.value("errorMessage").toString();
+
+ if(errorMessage.isEmpty())
+ {
+ return parseLegacyError(reply);
+ }
+
+ return tr("Login failed: ") + errorMessage;
+}
+
+void LoginTask::yggdrasilLogin()
+{
+ setStatus(tr("Logging in..."));
+ auto worker = MMC->qnam();
+ connect(worker.get(), SIGNAL(finished(QNetworkReply *)), this,
+ SLOT(processYggdrasilReply(QNetworkReply *)));
+
+ /*
+ {
+ // agent def. version might be incremented at some point
+ "agent":{"name":"Minecraft","version":1},
+ "username": "mojang account name",
+ "password": "mojang account password",
+ // client token is optional. but we supply one anyway
+ "clientToken": "client identifier"
+ }
+ */
+
+ QUrl loginURL("https://authserver.mojang.com/authenticate");
+ QNetworkRequest netRequest(loginURL);
+ netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ auto settings = MMC->settings();
+ QString clientToken = settings->get("YggdrasilClientToken").toString();
+ // escape the {}
+ clientToken.remove('{');
+ clientToken.remove('}');
+ // create the request
+ QString requestConstent;
+ requestConstent += "{";
+ requestConstent += " \"agent\":{\"name\":\"Minecraft\",\"version\":1},\n";
+ requestConstent += " \"username\":\"" + uInfo.username + "\",\n";
+ requestConstent += " \"password\":\"" + uInfo.password + "\",\n";
+ requestConstent += " \"clientToken\":\"" + clientToken + "\"\n";
+ requestConstent += "}";
+ netReply = worker->post(netRequest, requestConstent.toUtf8());
+}
+
+/*
+{
+ "accessToken": "random access token", // hexadecimal
+ "clientToken": "client identifier", // identical to the one received
+ "availableProfiles": [ // only present if the agent field was received
+ {
+ "id": "profile identifier", // hexadecimal
+ "name": "player name"
+ }
+ ],
+ "selectedProfile": { // only present if the agent field was received
+ "id": "profile identifier",
+ "name": "player name"
+ }
+}
+*/
+void LoginTask::parseYggdrasilReply(QByteArray data)
+{
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed(tr("Login failed: %1").arg(jsonError.errorString()));
+ return;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ emitFailed(tr("Login failed: BAD FORMAT #1"));
+ return;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ QString accessToken = root.value("accessToken").toString();
+ QString clientToken = root.value("clientToken").toString();
+ QString playerID;
+ QString playerName;
+ auto selectedProfile = root.value("selectedProfile");
+ if(selectedProfile.isObject())
+ {
+ auto selectedProfileO = selectedProfile.toObject();
+ playerID = selectedProfileO.value("id").toString();
+ playerName = selectedProfileO.value("name").toString();
+ }
+ QString sessionID = "token:" + accessToken + ":" + playerID;
+ /*
+ struct LoginResponse
+ {
+ QString username;
+ QString session_id;
+ QString player_name;
+ QString player_id;
+ QString client_id;
+ };
+ */
+
+ result = {uInfo.username, sessionID, playerName, playerID, accessToken};
+ emitSucceeded();
+}
diff --git a/logic/net/LoginTask.h b/logic/net/LoginTask.h
new file mode 100644
index 00000000..aa925999
--- /dev/null
+++ b/logic/net/LoginTask.h
@@ -0,0 +1,67 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "logic/tasks/Task.h"
+#include <QSharedPointer>
+
+struct UserInfo
+{
+ QString username;
+ QString password;
+};
+
+struct LoginResponse
+{
+ QString username;
+ QString session_id; // session id is a combination of player id and the access token
+ QString player_name;
+ QString player_id;
+ QString access_token;
+};
+
+class QNetworkReply;
+
+class LoginTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit LoginTask(const UserInfo &uInfo, QObject *parent = 0);
+ LoginResponse getResult()
+ {
+ return result;
+ }
+
+protected slots:
+ void legacyLogin();
+ void processLegacyReply(QNetworkReply *reply);
+ void parseLegacyReply(QByteArray data);
+ QString parseLegacyError(QNetworkReply *reply);
+
+ void yggdrasilLogin();
+ void processYggdrasilReply(QNetworkReply *reply);
+ void parseYggdrasilReply(QByteArray data);
+ QString parseYggdrasilError(QNetworkReply *reply);
+
+ void processReply(QNetworkReply *reply, std::function<void(LoginTask*, QByteArray)>, std::function<QString(LoginTask*, QNetworkReply*)>);
+
+protected:
+ void executeTask();
+
+ LoginResponse result;
+ QNetworkReply *netReply;
+ UserInfo uInfo;
+};