diff options
Diffstat (limited to 'logic/net')
-rw-r--r-- | logic/net/ByteArrayDownload.cpp | 82 | ||||
-rw-r--r-- | logic/net/ByteArrayDownload.h | 44 | ||||
-rw-r--r-- | logic/net/CacheDownload.cpp | 169 | ||||
-rw-r--r-- | logic/net/CacheDownload.h | 58 | ||||
-rw-r--r-- | logic/net/ForgeMirror.h | 10 | ||||
-rw-r--r-- | logic/net/ForgeMirrors.cpp | 118 | ||||
-rw-r--r-- | logic/net/ForgeMirrors.h | 58 | ||||
-rw-r--r-- | logic/net/ForgeXzDownload.cpp | 389 | ||||
-rw-r--r-- | logic/net/ForgeXzDownload.h | 65 | ||||
-rw-r--r-- | logic/net/HttpMetaCache.cpp | 253 | ||||
-rw-r--r-- | logic/net/HttpMetaCache.h | 75 | ||||
-rw-r--r-- | logic/net/MD5EtagDownload.cpp | 156 | ||||
-rw-r--r-- | logic/net/MD5EtagDownload.h | 51 | ||||
-rw-r--r-- | logic/net/NetAction.h | 89 | ||||
-rw-r--r-- | logic/net/NetJob.cpp | 112 | ||||
-rw-r--r-- | logic/net/NetJob.h | 124 | ||||
-rw-r--r-- | logic/net/PasteUpload.cpp | 86 | ||||
-rw-r--r-- | logic/net/PasteUpload.h | 26 | ||||
-rw-r--r-- | logic/net/URLConstants.h | 36 |
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 ¤t, 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"); +} |