summaryrefslogtreecommitdiffstats
path: root/logic/net
diff options
context:
space:
mode:
Diffstat (limited to 'logic/net')
-rw-r--r--logic/net/ByteArrayDownload.cpp82
-rw-r--r--logic/net/ByteArrayDownload.h44
-rw-r--r--logic/net/CacheDownload.cpp169
-rw-r--r--logic/net/CacheDownload.h58
-rw-r--r--logic/net/ForgeMirror.h10
-rw-r--r--logic/net/ForgeMirrors.cpp118
-rw-r--r--logic/net/ForgeMirrors.h58
-rw-r--r--logic/net/ForgeXzDownload.cpp389
-rw-r--r--logic/net/ForgeXzDownload.h65
-rw-r--r--logic/net/HttpMetaCache.cpp253
-rw-r--r--logic/net/HttpMetaCache.h75
-rw-r--r--logic/net/MD5EtagDownload.cpp156
-rw-r--r--logic/net/MD5EtagDownload.h51
-rw-r--r--logic/net/NetAction.h89
-rw-r--r--logic/net/NetJob.cpp112
-rw-r--r--logic/net/NetJob.h124
-rw-r--r--logic/net/PasteUpload.cpp86
-rw-r--r--logic/net/PasteUpload.h26
-rw-r--r--logic/net/URLConstants.h36
19 files changed, 2001 insertions, 0 deletions
diff --git a/logic/net/ByteArrayDownload.cpp b/logic/net/ByteArrayDownload.cpp
new file mode 100644
index 00000000..27d2a250
--- /dev/null
+++ b/logic/net/ByteArrayDownload.cpp
@@ -0,0 +1,82 @@
+/* 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 "ByteArrayDownload.h"
+#include "MultiMC.h"
+#include "logger/QsLog.h"
+
+ByteArrayDownload::ByteArrayDownload(QUrl url) : NetAction()
+{
+ 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)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit progress(m_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(m_index_within_job);
+ return;
+ }
+ // else the download failed
+ else
+ {
+ m_reply.reset();
+ emit failed(m_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..0d90abc2
--- /dev/null
+++ b/logic/net/ByteArrayDownload.h
@@ -0,0 +1,44 @@
+/* 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 "NetAction.h"
+
+typedef std::shared_ptr<class ByteArrayDownload> ByteArrayDownloadPtr;
+class ByteArrayDownload : public NetAction
+{
+ Q_OBJECT
+public:
+ ByteArrayDownload(QUrl url);
+ static ByteArrayDownloadPtr make(QUrl url)
+ {
+ return ByteArrayDownloadPtr(new ByteArrayDownload(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();
+};
diff --git a/logic/net/CacheDownload.cpp b/logic/net/CacheDownload.cpp
new file mode 100644
index 00000000..d2a9bdee
--- /dev/null
+++ b/logic/net/CacheDownload.cpp
@@ -0,0 +1,169 @@
+/* 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 "MultiMC.h"
+#include "CacheDownload.h"
+#include <pathutils.h>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QDateTime>
+#include "logger/QsLog.h"
+
+CacheDownload::CacheDownload(QUrl url, MetaEntryPtr entry)
+ : NetAction(), md5sum(QCryptographicHash::Md5)
+{
+ m_url = url;
+ m_entry = entry;
+ m_target_path = entry->getFullPath();
+ m_status = Job_NotStarted;
+}
+
+void CacheDownload::start()
+{
+ m_status = Job_InProgress;
+ if (!m_entry->stale)
+ {
+ m_status = Job_Finished;
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // create a new save file
+ m_output_file.reset(new QSaveFile(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))
+ {
+ QLOG_ERROR() << "Could not create folder for " + m_target_path;
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ if (!m_output_file->open(QIODevice::WriteOnly))
+ {
+ QLOG_ERROR() << "Could not open " + m_target_path + " for writing";
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ QLOG_INFO() << "Downloading " << m_url.toString();
+ QNetworkRequest request(m_url);
+
+ // check file consistency first.
+ QFile current(m_target_path);
+ if(current.exists() && current.size() != 0)
+ {
+ 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)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit progress(m_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)
+ {
+ m_output_file->cancelWriting();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ // if we wrote any data to the save file, we try to commit the data to the real file.
+ if (wroteAnyData)
+ {
+ // nothing went wrong...
+ if (m_output_file->commit())
+ {
+ m_status = Job_Finished;
+ m_entry->md5sum = md5sum.result().toHex().constData();
+ }
+ else
+ {
+ QLOG_ERROR() << "Failed to commit changes to " << m_target_path;
+ m_output_file->cancelWriting();
+ m_reply.reset();
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ else
+ {
+ m_status = Job_Finished;
+ }
+
+ // then get rid of the save file
+ m_output_file.reset();
+
+ 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(m_index_within_job);
+ return;
+}
+
+void CacheDownload::downloadReadyRead()
+{
+ QByteArray ba = m_reply->readAll();
+ md5sum.addData(ba);
+ if (m_output_file->write(ba) != ba.size())
+ {
+ QLOG_ERROR() << "Failed writing into " + m_target_path;
+ m_status = Job_Failed;
+ m_reply->abort();
+ emit failed(m_index_within_job);
+ }
+ wroteAnyData = true;
+}
diff --git a/logic/net/CacheDownload.h b/logic/net/CacheDownload.h
new file mode 100644
index 00000000..154f5988
--- /dev/null
+++ b/logic/net/CacheDownload.h
@@ -0,0 +1,58 @@
+/* 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 "NetAction.h"
+#include "HttpMetaCache.h"
+#include <QCryptographicHash>
+#include <QSaveFile>
+
+typedef std::shared_ptr<class CacheDownload> CacheDownloadPtr;
+class CacheDownload : public NetAction
+{
+ Q_OBJECT
+private:
+ MetaEntryPtr m_entry;
+ /// if saving to file, use the one specified in this string
+ QString m_target_path;
+ /// this is the output file, if any
+ std::shared_ptr<QSaveFile> m_output_file;
+ /// the hash-as-you-download
+ QCryptographicHash md5sum;
+
+ bool wroteAnyData = false;
+
+public:
+ explicit CacheDownload(QUrl url, MetaEntryPtr entry);
+ static CacheDownloadPtr make(QUrl url, MetaEntryPtr entry)
+ {
+ return CacheDownloadPtr(new CacheDownload(url, entry));
+ }
+ QString getTargetFilepath()
+ {
+ return m_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();
+};
diff --git a/logic/net/ForgeMirror.h b/logic/net/ForgeMirror.h
new file mode 100644
index 00000000..2518dffe
--- /dev/null
+++ b/logic/net/ForgeMirror.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <QString>
+
+struct ForgeMirror
+{
+ QString name;
+ QString logo_url;
+ QString website_url;
+ QString mirror_url;
+}; \ No newline at end of file
diff --git a/logic/net/ForgeMirrors.cpp b/logic/net/ForgeMirrors.cpp
new file mode 100644
index 00000000..b224306f
--- /dev/null
+++ b/logic/net/ForgeMirrors.cpp
@@ -0,0 +1,118 @@
+#include "MultiMC.h"
+#include "ForgeMirrors.h"
+#include "logger/QsLog.h"
+#include <algorithm>
+#include <random>
+
+ForgeMirrors::ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
+ QString mirrorlist)
+{
+ m_libs = libs;
+ m_parent_job = parent_job;
+ m_url = QUrl(mirrorlist);
+ m_status = Job_NotStarted;
+}
+
+void ForgeMirrors::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 ForgeMirrors::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 ForgeMirrors::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong... ?
+ parseMirrorList();
+ return;
+ }
+ // else the download failed, we use a fixed list
+ else
+ {
+ m_status = Job_Finished;
+ m_reply.reset();
+ deferToFixedList();
+ return;
+ }
+}
+
+void ForgeMirrors::deferToFixedList()
+{
+ m_mirrors.clear();
+ m_mirrors.append(
+ {"Minecraft Forge", "http://files.minecraftforge.net/forge_logo.png",
+ "http://files.minecraftforge.net/", "http://files.minecraftforge.net/maven/"});
+ m_mirrors.append({"Creeper Host",
+ "http://files.minecraftforge.net/forge_logo.png",
+ "https://www.creeperhost.net/link.php?id=1",
+ "http://new.creeperrepo.net/forge/maven/"});
+ injectDownloads();
+ emit succeeded(m_index_within_job);
+}
+
+void ForgeMirrors::parseMirrorList()
+{
+ m_status = Job_Finished;
+ auto data = m_reply->readAll();
+ m_reply.reset();
+ auto dataLines = data.split('\n');
+ for(auto line: dataLines)
+ {
+ auto elements = line.split('!');
+ if (elements.size() == 4)
+ {
+ m_mirrors.append({elements[0],elements[1],elements[2],elements[3]});
+ }
+ }
+ if(!m_mirrors.size())
+ deferToFixedList();
+ injectDownloads();
+ emit succeeded(m_index_within_job);
+}
+
+void ForgeMirrors::injectDownloads()
+{
+ // shuffle the mirrors randomly
+ std::random_device rd;
+ std::mt19937 rng(rd());
+ std::shuffle(m_mirrors.begin(), m_mirrors.end(), rng);
+
+ // tell parent to download the libs
+ for(auto lib: m_libs)
+ {
+ lib->setMirrors(m_mirrors);
+ m_parent_job->addNetAction(lib);
+ }
+}
+
+void ForgeMirrors::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit progress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void ForgeMirrors::downloadReadyRead()
+{
+}
diff --git a/logic/net/ForgeMirrors.h b/logic/net/ForgeMirrors.h
new file mode 100644
index 00000000..990e49d6
--- /dev/null
+++ b/logic/net/ForgeMirrors.h
@@ -0,0 +1,58 @@
+/* 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 "NetAction.h"
+#include "HttpMetaCache.h"
+#include "ForgeXzDownload.h"
+#include "NetJob.h"
+#include <QFile>
+#include <QTemporaryFile>
+typedef std::shared_ptr<class ForgeMirrors> ForgeMirrorsPtr;
+
+class ForgeMirrors : public NetAction
+{
+ Q_OBJECT
+public:
+ QList<ForgeXzDownloadPtr> m_libs;
+ NetJobPtr m_parent_job;
+ QList<ForgeMirror> m_mirrors;
+
+public:
+ explicit ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
+ QString mirrorlist);
+ static ForgeMirrorsPtr make(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
+ QString mirrorlist)
+ {
+ return ForgeMirrorsPtr(new ForgeMirrors(libs, parent_job, mirrorlist));
+ }
+
+protected
+slots:
+ virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+ virtual void downloadError(QNetworkReply::NetworkError error);
+ virtual void downloadFinished();
+ virtual void downloadReadyRead();
+
+private:
+ void parseMirrorList();
+ void deferToFixedList();
+ void injectDownloads();
+
+public
+slots:
+ virtual void start();
+};
diff --git a/logic/net/ForgeXzDownload.cpp b/logic/net/ForgeXzDownload.cpp
new file mode 100644
index 00000000..359ad858
--- /dev/null
+++ b/logic/net/ForgeXzDownload.cpp
@@ -0,0 +1,389 @@
+/* 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 "MultiMC.h"
+#include "ForgeXzDownload.h"
+#include <pathutils.h>
+
+#include <QCryptographicHash>
+#include <QFileInfo>
+#include <QDateTime>
+#include <QDir>
+#include "logger/QsLog.h"
+
+ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction()
+{
+ m_entry = entry;
+ m_target_path = entry->getFullPath();
+ m_pack200_xz_file.setFileTemplate("./dl_temp.XXXXXX");
+ m_status = Job_NotStarted;
+ m_url_path = relative_path;
+}
+
+void ForgeXzDownload::setMirrors(QList<ForgeMirror> &mirrors)
+{
+ m_mirror_index = 0;
+ m_mirrors = mirrors;
+ updateUrl();
+}
+
+void ForgeXzDownload::start()
+{
+ m_status = Job_InProgress;
+ if (!m_entry->stale)
+ {
+ m_status = Job_Finished;
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // can we actually create the real, final file?
+ if (!ensureFilePathExists(m_target_path))
+ {
+ m_status = Job_Failed;
+ emit failed(m_index_within_job);
+ return;
+ }
+ if (m_mirrors.empty())
+ {
+ m_status = Job_Failed;
+ emit failed(m_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)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit progress(m_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::failAndTryNextMirror()
+{
+ m_status = Job_Failed;
+ int next = m_mirror_index + 1;
+ if(m_mirrors.size() == next)
+ m_mirror_index = 0;
+ else
+ m_mirror_index = next;
+
+ updateUrl();
+ emit failed(m_index_within_job);
+}
+
+void ForgeXzDownload::updateUrl()
+{
+ QLOG_INFO() << "Updating URL for " << m_url_path;
+ for (auto possible : m_mirrors)
+ {
+ QLOG_INFO() << "Possible: " << possible.name << " : " << possible.mirror_url;
+ }
+ QString aggregate = m_mirrors[m_mirror_index].mirror_url + m_url_path + ".pack.xz";
+ m_url = QUrl(aggregate);
+}
+
+void ForgeXzDownload::downloadFinished()
+{
+ //TEST: defer to other possible mirrors (autofail the first one)
+ /*
+ QLOG_INFO() <<"dl " << index_within_job << " mirror " << m_mirror_index;
+ if( m_mirror_index == 0)
+ {
+ QLOG_INFO() <<"dl " << index_within_job << " AUTOFAIL";
+ m_status = Job_Failed;
+ m_pack200_xz_file.close();
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ failAndTryNextMirror();
+ return;
+ }
+ */
+
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ if (m_pack200_xz_file.isOpen())
+ {
+ // we actually downloaded something! process and isntall it
+ decompressAndInstall();
+ return;
+ }
+ else
+ {
+ // something bad happened -- on the local machine!
+ m_status = Job_Failed;
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ // else the download failed
+ else
+ {
+ m_status = Job_Failed;
+ m_pack200_xz_file.close();
+ m_pack200_xz_file.remove();
+ m_reply.reset();
+ failAndTryNextMirror();
+ return;
+ }
+}
+
+void ForgeXzDownload::downloadReadyRead()
+{
+
+ if (!m_pack200_xz_file.isOpen())
+ {
+ if (!m_pack200_xz_file.open())
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ 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("./dl_temp.XXXXXX");
+ 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);
+ failAndTryNextMirror();
+ 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);
+ failAndTryNextMirror();
+ 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);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_MEMLIMIT_ERROR:
+ QLOG_ERROR() << "Memory usage limit reached\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_FORMAT_ERROR:
+ QLOG_ERROR() << "Not a .xz file\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_OPTIONS_ERROR:
+ QLOG_ERROR() << "Unsupported options in the .xz headers\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ case XZ_DATA_ERROR:
+ case XZ_BUF_ERROR:
+ QLOG_ERROR() << "File is corrupt\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+
+ default:
+ QLOG_ERROR() << "Bug!\n";
+ xz_dec_end(s);
+ failAndTryNextMirror();
+ return;
+ }
+ }
+ }
+ m_pack200_xz_file.remove();
+
+ // revert pack200
+ pack200_file.seek(0);
+ int handle_in = pack200_file.handle();
+ // FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects.
+ if(handle_in == -1)
+ {
+ QLOG_ERROR() << "Error reopening " << pack200_file.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ FILE * file_in = fdopen(handle_in,"r");
+ if(!file_in)
+ {
+ QLOG_ERROR() << "Error reopening " << pack200_file.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ QFile qfile_out(m_target_path);
+ if(!qfile_out.open(QIODevice::WriteOnly))
+ {
+ QLOG_ERROR() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ int handle_out = qfile_out.handle();
+ if(handle_out == -1)
+ {
+ QLOG_ERROR() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ FILE * file_out = fdopen(handle_out,"w");
+ if(!file_out)
+ {
+ QLOG_ERROR() << "Error opening " << qfile_out.fileName();
+ failAndTryNextMirror();
+ return;
+ }
+ try
+ {
+ unpack_200(file_in, file_out);
+ }
+ catch (std::runtime_error &err)
+ {
+ m_status = Job_Failed;
+ QLOG_ERROR() << "Error unpacking " << pack200_file.fileName() << " : " << err.what();
+ QFile f(m_target_path);
+ if (f.exists())
+ f.remove();
+ failAndTryNextMirror();
+ return;
+ }
+ pack200_file.remove();
+
+ QFile jar_file(m_target_path);
+
+ if (!jar_file.open(QIODevice::ReadOnly))
+ {
+ jar_file.remove();
+ failAndTryNextMirror();
+ 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(m_index_within_job);
+}
diff --git a/logic/net/ForgeXzDownload.h b/logic/net/ForgeXzDownload.h
new file mode 100644
index 00000000..990f91f0
--- /dev/null
+++ b/logic/net/ForgeXzDownload.h
@@ -0,0 +1,65 @@
+/* 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 "NetAction.h"
+#include "HttpMetaCache.h"
+#include <QFile>
+#include <QTemporaryFile>
+#include "ForgeMirror.h"
+
+typedef std::shared_ptr<class ForgeXzDownload> ForgeXzDownloadPtr;
+
+class ForgeXzDownload : public NetAction
+{
+ Q_OBJECT
+public:
+ MetaEntryPtr m_entry;
+ /// 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;
+ /// mirror index (NOT OPTICS, I SWEAR)
+ int m_mirror_index = 0;
+ /// list of mirrors to use. Mirror has the url base
+ QList<ForgeMirror> m_mirrors;
+ /// path relative to the mirror base
+ QString m_url_path;
+
+public:
+ explicit ForgeXzDownload(QString relative_path, MetaEntryPtr entry);
+ static ForgeXzDownloadPtr make(QString relative_path, MetaEntryPtr entry)
+ {
+ return ForgeXzDownloadPtr(new ForgeXzDownload(relative_path, entry));
+ }
+ void setMirrors(QList<ForgeMirror> & mirrors);
+
+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();
+ void failAndTryNextMirror();
+ void updateUrl();
+};
diff --git a/logic/net/HttpMetaCache.cpp b/logic/net/HttpMetaCache.cpp
new file mode 100644
index 00000000..29007951
--- /dev/null
+++ b/logic/net/HttpMetaCache.cpp
@@ -0,0 +1,253 @@
+/* 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 "MultiMC.h"
+#include "HttpMetaCache.h"
+#include <pathutils.h>
+
+#include <QFileInfo>
+#include <QFile>
+#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()
+{
+ saveBatchingTimer.stop();
+ SaveNow();
+}
+
+MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path)
+{
+ // no base. no base path. can't store
+ if (!m_entries.contains(base))
+ {
+ // 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);
+
+ // is the file really there? if not -> stale
+ if (!finfo.isFile() || !finfo.isReadable())
+ {
+ // 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);
+ }
+
+ // 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)
+{
+ // TODO: report error
+ if (m_entries.contains(base))
+ return;
+ // TODO: check if the base path is valid
+ EntryMap foo;
+ foo.base_path = 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);
+ if (!index.open(QIODevice::ReadOnly))
+ return;
+
+ QJsonDocument json = QJsonDocument::fromJson(index.readAll());
+ if (!json.isObject())
+ return;
+ auto root = json.object();
+ // check file version first
+ auto version_val = root.value("version");
+ if (!version_val.isString())
+ return;
+ if (version_val.toString() != "1")
+ return;
+
+ // read the entry array
+ auto entries_val = root.value("entries");
+ if (!entries_val.isArray())
+ return;
+ QJsonArray array = entries_val.toArray();
+ for (auto element : array)
+ {
+ if (!element.isObject())
+ return;
+ auto element_obj = element.toObject();
+ QString base = element_obj.value("base").toString();
+ if (!m_entries.contains(base))
+ continue;
+ auto &entrymap = m_entries[base];
+ auto foo = new MetaEntry;
+ foo->base = base;
+ QString path = foo->path = element_obj.value("path").toString();
+ foo->md5sum = element_obj.value("md5sum").toString();
+ foo->etag = element_obj.value("etag").toString();
+ 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::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))
+ return;
+ QJsonObject toplevel;
+ toplevel.insert("version", QJsonValue(QString("1")));
+ QJsonArray entriesArr;
+ for (auto group : m_entries)
+ {
+ for (auto entry : group.entry_list)
+ {
+ QJsonObject entryObj;
+ entryObj.insert("base", QJsonValue(entry->base));
+ 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->local_changed_timestamp)));
+ if (!entry->remote_changed_timestamp.isEmpty())
+ entryObj.insert("remote_changed_timestamp",
+ QJsonValue(entry->remote_changed_timestamp));
+ entriesArr.append(entryObj);
+ }
+ }
+ toplevel.insert("entries", entriesArr);
+ QJsonDocument doc(toplevel);
+ QByteArray jsonData = doc.toJson();
+ qint64 result = tfile.write(jsonData);
+ if (result == -1)
+ return;
+ if (result != jsonData.size())
+ return;
+ tfile.commit();
+}
diff --git a/logic/net/HttpMetaCache.h b/logic/net/HttpMetaCache.h
new file mode 100644
index 00000000..08b39fe2
--- /dev/null
+++ b/logic/net/HttpMetaCache.h
@@ -0,0 +1,75 @@
+/* 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 <QString>
+#include <QMap>
+#include <qtimer.h>
+
+struct MetaEntry
+{
+ QString base;
+ QString path;
+ QString md5sum;
+ QString etag;
+ qint64 local_changed_timestamp = 0;
+ QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time
+ bool stale = true;
+ QString getFullPath();
+};
+
+typedef std::shared_ptr<MetaEntry> MetaEntryPtr;
+
+class HttpMetaCache : public QObject
+{
+ Q_OBJECT
+public:
+ // supply path to the cache index file
+ HttpMetaCache(QString path);
+ ~HttpMetaCache();
+
+ // 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);
+
+ // (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;
+ QMap<QString, MetaEntryPtr> entry_list;
+ };
+ QMap<QString, EntryMap> m_entries;
+ QString m_index_file;
+ QTimer saveBatchingTimer;
+}; \ No newline at end of file
diff --git a/logic/net/MD5EtagDownload.cpp b/logic/net/MD5EtagDownload.cpp
new file mode 100644
index 00000000..63583e8d
--- /dev/null
+++ b/logic/net/MD5EtagDownload.cpp
@@ -0,0 +1,156 @@
+/* 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 "MultiMC.h"
+#include "MD5EtagDownload.h"
+#include <pathutils.h>
+#include <QCryptographicHash>
+#include "logger/QsLog.h"
+
+MD5EtagDownload::MD5EtagDownload(QUrl url, QString target_path) : NetAction()
+{
+ m_url = url;
+ m_target_path = target_path;
+ m_status = Job_NotStarted;
+}
+
+void MD5EtagDownload::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))
+ {
+ // get the md5 of the local file.
+ m_local_md5 =
+ QCryptographicHash::hash(m_output_file.readAll(), QCryptographicHash::Md5)
+ .toHex()
+ .constData();
+ m_output_file.close();
+ // if we are expecting some md5sum, compare it with the local one
+ if (!m_expected_md5.isEmpty())
+ {
+ // skip if they match
+ if(m_local_md5 == m_expected_md5)
+ {
+ QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match.";
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ }
+ else
+ {
+ // no expected md5. we use the local md5sum as an ETag
+ }
+ }
+ if (!ensureFilePathExists(filename))
+ {
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ QNetworkRequest request(m_url);
+
+ QLOG_INFO() << "Downloading " << m_url.toString() << " got " << m_local_md5;
+
+ if(!m_local_md5.isEmpty())
+ {
+ QLOG_INFO() << "Got " << m_local_md5;
+ request.setRawHeader(QString("If-None-Match").toLatin1(), m_local_md5.toLatin1());
+ }
+ if(!m_expected_md5.isEmpty())
+ QLOG_INFO() << "Expecting " << m_expected_md5;
+
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+
+ // Go ahead and try to open the file.
+ // If we don't do this, empty files won't be created, which breaks the updater.
+ // Plus, this way, we don't end up starting a download for a file we can't open.
+ if (!m_output_file.open(QIODevice::WriteOnly))
+ {
+ emit failed(m_index_within_job);
+ return;
+ }
+
+ 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 MD5EtagDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+ m_total_progress = bytesTotal;
+ m_progress = bytesReceived;
+ emit progress(m_index_within_job, bytesReceived, bytesTotal);
+}
+
+void MD5EtagDownload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ // TODO: log the reason why
+ m_status = Job_Failed;
+}
+
+void MD5EtagDownload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_status != Job_Failed)
+ {
+ // nothing went wrong...
+ m_status = Job_Finished;
+ m_output_file.close();
+
+ // FIXME: compare with the real written data md5sum
+ // this is just an ETag
+ QLOG_INFO() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData();
+
+ m_reply.reset();
+ emit succeeded(m_index_within_job);
+ return;
+ }
+ // else the download failed
+ else
+ {
+ m_output_file.close();
+ m_output_file.remove();
+ m_reply.reset();
+ emit failed(m_index_within_job);
+ return;
+ }
+}
+
+void MD5EtagDownload::downloadReadyRead()
+{
+ if (!m_output_file.isOpen())
+ {
+ if (!m_output_file.open(QIODevice::WriteOnly))
+ {
+ /*
+ * Can't open the file... the job failed
+ */
+ m_reply->abort();
+ emit failed(m_index_within_job);
+ return;
+ }
+ }
+ m_output_file.write(m_reply->readAll());
+}
diff --git a/logic/net/MD5EtagDownload.h b/logic/net/MD5EtagDownload.h
new file mode 100644
index 00000000..d5aed0ca
--- /dev/null
+++ b/logic/net/MD5EtagDownload.h
@@ -0,0 +1,51 @@
+/* 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 "NetAction.h"
+#include <QFile>
+
+typedef std::shared_ptr<class MD5EtagDownload> Md5EtagDownloadPtr;
+class MD5EtagDownload : public NetAction
+{
+ Q_OBJECT
+public:
+ /// the expected md5 checksum. Only set from outside
+ QString m_expected_md5;
+ /// the md5 checksum of a file that already exists.
+ QString m_local_md5;
+ /// 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 MD5EtagDownload(QUrl url, QString target_path);
+ static Md5EtagDownloadPtr make(QUrl url, QString target_path)
+ {
+ return Md5EtagDownloadPtr(new MD5EtagDownload(url, 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();
+};
diff --git a/logic/net/NetAction.h b/logic/net/NetAction.h
new file mode 100644
index 00000000..97c96e5d
--- /dev/null
+++ b/logic/net/NetAction.h
@@ -0,0 +1,89 @@
+/* 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 <QObject>
+#include <QUrl>
+#include <memory>
+#include <QNetworkReply>
+
+enum JobStatus
+{
+ Job_NotStarted,
+ Job_InProgress,
+ Job_Finished,
+ Job_Failed
+};
+
+typedef std::shared_ptr<class NetAction> NetActionPtr;
+class NetAction : public QObject
+{
+ Q_OBJECT
+protected:
+ explicit NetAction() : QObject(0) {};
+
+public:
+ virtual ~NetAction() {};
+
+public:
+ virtual qint64 totalProgress() const
+ {
+ return m_total_progress;
+ }
+ virtual qint64 currentProgress() const
+ {
+ return m_progress;
+ }
+ virtual qint64 numberOfFailures() const
+ {
+ return m_failures;
+ }
+public:
+ /// the network reply
+ std::shared_ptr<QNetworkReply> m_reply;
+
+ /// source URL
+ QUrl m_url;
+
+ /// The file's status
+ JobStatus m_status = Job_NotStarted;
+
+ /// index within the parent job
+ int m_index_within_job = 0;
+
+ qint64 m_progress = 0;
+ qint64 m_total_progress = 1;
+
+ /// number of failures up to this point
+ int m_failures = 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;
+};
diff --git a/logic/net/NetJob.cpp b/logic/net/NetJob.cpp
new file mode 100644
index 00000000..9e800d13
--- /dev/null
+++ b/logic/net/NetJob.cpp
@@ -0,0 +1,112 @@
+/* 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 "NetJob.h"
+#include "pathutils.h"
+#include "MultiMC.h"
+#include "MD5EtagDownload.h"
+#include "ByteArrayDownload.h"
+#include "CacheDownload.h"
+
+#include "logger/QsLog.h"
+
+void NetJob::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++;
+ QLOG_INFO() << m_job_name.toLocal8Bit() << "progress:" << num_succeeded << "/"
+ << downloads.size();
+
+ if (num_failed + num_succeeded == downloads.size())
+ {
+ if (num_failed)
+ {
+ QLOG_ERROR() << m_job_name.toLocal8Bit() << "failed.";
+ emit failed();
+ }
+ else
+ {
+ QLOG_INFO() << m_job_name.toLocal8Bit() << "succeeded.";
+ emit succeeded();
+ }
+ }
+}
+
+void NetJob::partFailed(int index)
+{
+ auto &slot = parts_progress[index];
+ if (slot.failures == 3)
+ {
+ 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 NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
+{
+ 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 NetJob::start()
+{
+ QLOG_INFO() << m_job_name.toLocal8Bit() << " started.";
+ m_running = true;
+ for (auto iter : downloads)
+ {
+ 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 NetJob::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/NetJob.h b/logic/net/NetJob.h
new file mode 100644
index 00000000..03d6a36e
--- /dev/null
+++ b/logic/net/NetJob.h
@@ -0,0 +1,124 @@
+/* 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 <QtNetwork>
+#include <QLabel>
+#include "NetAction.h"
+#include "ByteArrayDownload.h"
+#include "MD5EtagDownload.h"
+#include "CacheDownload.h"
+#include "HttpMetaCache.h"
+#include "ForgeXzDownload.h"
+#include "logic/tasks/ProgressProvider.h"
+
+class NetJob;
+typedef std::shared_ptr<NetJob> NetJobPtr;
+
+class NetJob : public ProgressProvider
+{
+ Q_OBJECT
+public:
+ explicit NetJob(QString job_name) : ProgressProvider(), m_job_name(job_name) {};
+
+ template <typename T> bool addNetAction(T action)
+ {
+ NetActionPtr base = std::static_pointer_cast<NetAction>(action);
+ base->m_index_within_job = downloads.size();
+ downloads.append(action);
+ part_info pi;
+ {
+ pi.current_progress = base->currentProgress();
+ pi.total_progress = base->totalProgress();
+ pi.failures = base->numberOfFailures();
+ }
+ parts_progress.append(pi);
+ total_progress += pi.total_progress;
+ // if this is already running, the action needs to be started right away!
+ if (isRunning())
+ {
+ emit progress(current_progress, total_progress);
+ connect(base.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int)));
+ connect(base.get(), SIGNAL(failed(int)), SLOT(partFailed(int)));
+ connect(base.get(), SIGNAL(progress(int, qint64, qint64)),
+ SLOT(partProgress(int, qint64, qint64)));
+ base->start();
+ }
+ return true;
+ }
+
+ NetActionPtr operator[](int index)
+ {
+ return downloads[index];
+ }
+ ;
+ NetActionPtr first()
+ {
+ if (downloads.size())
+ return downloads[0];
+ return NetActionPtr();
+ }
+ int size() const
+ {
+ 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);
+ void succeeded();
+ void failed();
+public
+slots:
+ virtual void start();
+ // FIXME: implement
+ virtual void abort() {};
+private
+slots:
+ void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal);
+ 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<NetActionPtr> 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/PasteUpload.cpp b/logic/net/PasteUpload.cpp
new file mode 100644
index 00000000..fa54d084
--- /dev/null
+++ b/logic/net/PasteUpload.cpp
@@ -0,0 +1,86 @@
+#include "PasteUpload.h"
+#include "MultiMC.h"
+#include "logger/QsLog.h"
+#include <QJsonObject>
+#include <QJsonDocument>
+#include "gui/dialogs/CustomMessageBox.h"
+#include <QDesktopServices>
+
+PasteUpload::PasteUpload(QWidget *window, QString text) : m_text(text), m_window(window)
+{
+}
+
+void PasteUpload::executeTask()
+{
+ QNetworkRequest request(QUrl("http://paste.ee/api"));
+ request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
+ QByteArray content(
+ "key=public&description=MultiMC5+Log+File&language=plain&format=json&paste=" +
+ m_text.toUtf8());
+ request.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
+ request.setRawHeader("Content-Length", QByteArray::number(content.size()));
+
+ auto worker = MMC->qnam();
+ QNetworkReply *rep = worker->post(request, content);
+
+ m_reply = std::shared_ptr<QNetworkReply>(rep);
+ connect(rep, &QNetworkReply::downloadProgress, [&](qint64 value, qint64 max)
+ { setProgress(value / max * 100); });
+ connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this,
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void PasteUpload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ QLOG_ERROR() << "Network error: " << error;
+ emitFailed(m_reply->errorString());
+}
+
+void PasteUpload::downloadFinished()
+{
+ // if the download succeeded
+ if (m_reply->error() == QNetworkReply::NetworkError::NoError)
+ {
+ QByteArray data = m_reply->readAll();
+ m_reply.reset();
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ emitFailed(jsonError.errorString());
+ return;
+ }
+ QString error;
+ if (!parseResult(doc, &error))
+ {
+ emitFailed(error);
+ return;
+ }
+ }
+ // else the download failed
+ else
+ {
+ emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
+ m_reply.reset();
+ return;
+ }
+ emitSucceeded();
+}
+
+bool PasteUpload::parseResult(QJsonDocument doc, QString *parseError)
+{
+ auto object = doc.object();
+ auto status = object.value("status").toString("error");
+ if (status == "error")
+ {
+ parseError = new QString(object.value("error").toString());
+ return false;
+ }
+ // FIXME: not the place for GUI things.
+ QString pasteUrl = object.value("paste").toObject().value("link").toString();
+ QDesktopServices::openUrl(pasteUrl);
+ return true;
+}
+
diff --git a/logic/net/PasteUpload.h b/logic/net/PasteUpload.h
new file mode 100644
index 00000000..917a0016
--- /dev/null
+++ b/logic/net/PasteUpload.h
@@ -0,0 +1,26 @@
+#pragma once
+#include "logic/tasks/Task.h"
+#include <QMessageBox>
+#include <QNetworkReply>
+#include <memory>
+
+class PasteUpload : public Task
+{
+ Q_OBJECT
+public:
+ PasteUpload(QWidget *window, QString text);
+
+protected:
+ virtual void executeTask();
+
+private:
+ bool parseResult(QJsonDocument doc, QString *parseError);
+ QString m_text;
+ QString m_error;
+ QWidget *m_window;
+ std::shared_ptr<QNetworkReply> m_reply;
+public
+slots:
+ void downloadError(QNetworkReply::NetworkError);
+ void downloadFinished();
+};
diff --git a/logic/net/URLConstants.h b/logic/net/URLConstants.h
new file mode 100644
index 00000000..8cb1f3fd
--- /dev/null
+++ b/logic/net/URLConstants.h
@@ -0,0 +1,36 @@
+/* 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 <QString>
+
+namespace URLConstants
+{
+const QString AWS_DOWNLOAD_BASE("s3.amazonaws.com/Minecraft.Download/");
+const QString AWS_DOWNLOAD_VERSIONS(AWS_DOWNLOAD_BASE + "versions/");
+const QString AWS_DOWNLOAD_LIBRARIES(AWS_DOWNLOAD_BASE + "libraries/");
+const QString AWS_DOWNLOAD_INDEXES(AWS_DOWNLOAD_BASE + "indexes/");
+const QString ASSETS_BASE("assets.minecraft.net/");
+//const QString MCN_BASE("sonicrules.org/mcnweb.py");
+const QString RESOURCE_BASE("resources.download.minecraft.net/");
+const QString LIBRARY_BASE("libraries.minecraft.net/");
+const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/");
+const QString AUTH_BASE("authserver.mojang.com/");
+const QString FORGE_LEGACY_URL("http://files.minecraftforge.net/minecraftforge/json");
+const QString FORGE_GRADLE_URL("http://files.minecraftforge.net/maven/net/minecraftforge/forge/json");
+const QString MOJANG_STATUS_URL("http://status.mojang.com/check");
+const QString MOJANG_STATUS_NEWS_URL("http://status.mojang.com/news");
+}